diff --git a/ivette/package.json b/ivette/package.json index e44cabd9e3d998792d6dd0f9d6896ad0a08a4cba..a443b467f10619e0554b46a7f8b740b19863c915 100644 --- a/ivette/package.json +++ b/ivette/package.json @@ -48,6 +48,7 @@ "react-dom": "^18", "react-draggable": "^4.4.6", "react-fast-compare": "^3.2.2", + "react-flame-graph" : "^1.4.0", "react-force-graph-2d": "^1.25.4", "react-force-graph-3d": "^1.24.2", "react-infinite-scroller": "^1.2.6", diff --git a/ivette/src/dome/renderer/controls/gallery.json b/ivette/src/dome/renderer/controls/gallery.json index 36bc7aa800032da1d9b372d45f89656444206013..d5d97ee59f91c835dd423ccec0a110e9964ac2e1 100644 --- a/ivette/src/dome/renderer/controls/gallery.json +++ b/ivette/src/dome/renderer/controls/gallery.json @@ -316,6 +316,12 @@ "viewBox": "0 0 24 24", "path": "M15.428 18.429v-2.143q0-0.188-0.121-0.308t-0.308-0.121h-1.286v-6.857q0-0.188-0.121-0.308t-0.308-0.121h-4.286q-0.188 0-0.308 0.121t-0.121 0.308v2.143q0 0.188 0.121 0.308t0.308 0.121h1.286v4.286h-1.286q-0.188 0-0.308 0.121t-0.121 0.308v2.143q0 0.188 0.121 0.308t0.308 0.121h6q0.188 0 0.308-0.121t0.121-0.308zM13.714 6.429v-2.143q0-0.188-0.121-0.308t-0.308-0.121h-2.571q-0.188 0-0.308 0.121t-0.121 0.308v2.143q0 0.188 0.121 0.308t0.308 0.121h2.571q0.188 0 0.308-0.121t0.121-0.308zM22.285 12q0 2.799-1.379 5.163t-3.743 3.743-5.163 1.379-5.163-1.379-3.743-3.743-1.379-5.163 1.379-5.163 3.743-3.743 5.163-1.379 5.163 1.379 3.743 3.743 1.379 5.163z" }, + "TARGET": { + "section": "Buttons", + "title": "Target", + "viewBox": "0 0 32 32", + "path": "M32 14h-3.154c-0.864-5.57-5.276-9.982-10.846-10.846v-3.154h-4v3.154c-5.57 0.864-9.982 5.276-10.846 10.846h-3.154v4h3.154c0.864 5.57 5.276 9.982 10.846 10.846v3.154h4v-3.154c5.57-0.864 9.982-5.276 10.846-10.846h3.154v-4zM24.776 14h-3.118c-0.603-1.705-1.953-3.056-3.658-3.658v-3.118c3.36 0.765 6.010 3.416 6.776 6.776zM16 18c-1.105 0-2-0.895-2-2s0.895-2 2-2c1.105 0 2 0.895 2 2s-0.895 2-2 2zM14 7.224v3.118c-1.705 0.603-3.056 1.953-3.658 3.658h-3.118c0.765-3.36 3.416-6.010 6.776-6.776zM7.224 18h3.118c0.603 1.705 1.953 3.056 3.658 3.658v3.118c-3.36-0.765-6.010-3.416-6.776-6.776zM18 24.776v-3.118c1.705-0.603 3.056-1.953 3.658-3.658h3.118c-0.765 3.36-3.416 6.010-6.776 6.776z" + }, "SWITCH.OFF": { "section": "Buttons", "title": "Off", diff --git a/ivette/src/frama-c/plugins/eva/Flamegraph.tsx b/ivette/src/frama-c/plugins/eva/Flamegraph.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41f01b1f881070e9da2616a5f707b34e51590433 --- /dev/null +++ b/ivette/src/frama-c/plugins/eva/Flamegraph.tsx @@ -0,0 +1,178 @@ +/* ************************************************************************ */ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2024 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/* ************************************************************************ */ + +import React from 'react'; +import { IconButton } from 'dome/controls/buttons'; +import * as Ivette from 'ivette'; +import * as Ast from 'frama-c/kernel/api/ast'; +import * as States from 'frama-c/states'; +import * as Eva from 'frama-c/plugins/eva/api/general'; +import { FlameGraph } from 'react-flame-graph'; +import AutoSizer, { Size } from 'react-virtualized-auto-sizer'; +import { EvaReady, EvaStatus } from './components/AnalysisStatus'; +import { Inset } from 'dome/frame/toolbars'; +import { useFlipSettings } from 'dome'; + +// --- Flamegraph Table --- +interface Flamegraph { + kfKey?: string; + name: string; + value: number; + children?: Flamegraph[]; +} + +const addNodeToFlamegraph = ( + flamegraph: Flamegraph, + cs: string[], + row: Eva.evaFlamegraphData, +): void => { + // Accumulate times for all nodes crossed + flamegraph.value += row.time; + // updating last node + if(cs.length === 0) { + flamegraph.kfKey = row.kfkey; + return; + } + // Search/create next node + if (!flamegraph.children) flamegraph.children = []; + let nextNode = flamegraph.children.find((elt) => elt.name === cs[0]); + if (!nextNode) { + nextNode = { name: cs[0], value: 0 }; + flamegraph.children.unshift(nextNode); + } + cs.shift(); + // Treatment of the next node + addNodeToFlamegraph(nextNode, cs, row); +}; + +interface EvaFlamegraphProps { + useScope: boolean; + flameGraph: Flamegraph; + size: Size +} + +/* Round f to at most [decimal] decimals. */ +function round(f: number, decimal: number): number { + const factor = 10 ** decimal; + return Math.round(f * factor) / factor; +} + +/* Returns text to be shown about a node in a flamegraph. */ +function nodeInfoText(flameGraph:Flamegraph, node:Flamegraph): string { + const percentage = round(100 * node.value / flameGraph.value, 1); + const value = round(node.value, 2); + const infos = node.name + " : " + value + "s : " + percentage + "%"; + return infos; +} + +function EvaFlamegraph(props: EvaFlamegraphProps): JSX.Element { + const { useScope, flameGraph, size } = props; + const { width, height } = size; + const [ nodeInfos, setNodeInfos ] = React.useState(""); + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + const changeScope = (node:any): void => { + if (useScope) States.setCurrentScope(node.source.kfKey as Ast.decl); + }; + + return ( + <> + <FlameGraph + data={flameGraph} + height={height} + width={width} + onChange={changeScope} + onMouseOver={(_e:Event, node:Flamegraph) => { + setNodeInfos(nodeInfoText(flameGraph, node)); + }} + onMouseOut={() => { setNodeInfos(""); }} + /> + { + nodeInfos && + <div className='flame-details'> + {nodeInfos} + </div> + } + </> + ); +} + +// --- Flamegraph Component --- +export function FlamegraphComponent(): JSX.Element { + const [useScope, flipUseScope] = + useFlipSettings("eva.flamegraph.scope", true); + const model = States.useSyncArrayData(Eva.evaFlamegraph); + + const flameGraph = React.useMemo<Flamegraph | null>(() => { + if(model.length === 0 ) return null; + const flame: Flamegraph = { + name: model[0].funlist.split(":")[0], + value: 0 + }; + model.forEach(row => { + const cs = row.funlist.split(":"); + cs.shift(); + addNodeToFlamegraph(flame, cs, row); + }); + return flame; + }, [model]); + + const isWaitingForData = !flameGraph || !flameGraph.children; + + return ( + <> + <Ivette.TitleBar > + <IconButton + icon="PIN" + kind={useScope ? "positive" : "default"} + onClick={flipUseScope} + title={useScope ? "Scope change enabled" : "Scope change disabled"} + /> + <Inset /> + <EvaStatus /> + </Ivette.TitleBar> + <EvaReady showChildrenForComputingStatus={!isWaitingForData} > + { + !isWaitingForData && + <AutoSizer key="flamegraph"> + {(size: Size) => ( + <EvaFlamegraph + useScope={useScope} + flameGraph={flameGraph} + size={size} + /> + )} + </AutoSizer> + } + </EvaReady> + </> + ); +} + +Ivette.registerComponent({ + id: 'fc.eva.flamegraph', + label: 'Eva Flamegraph', + title: 'Detailed flamegraph of the Eva analysis', + children: <FlamegraphComponent />, +}); + +// -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/plugins/eva/api/general/index.ts b/ivette/src/frama-c/plugins/eva/api/general/index.ts index c37d5121b0b153e16622f7c4a2a01e83b54d3c20..ce5eb30dbab5659ea3483db89c6d6ad16931becb 100644 --- a/ivette/src/frama-c/plugins/eva/api/general/index.ts +++ b/ivette/src/frama-c/plugins/eva/api/general/index.ts @@ -760,4 +760,145 @@ export const getStates: Server.GetRequest< [ string, string, string ][] >= getStates_internal; +/** Kernel function stack identifier */ +export type kfstack = Json.index<'#kfstack'>; + +/** Decoder for `kfstack` */ +export const jKfstack: Json.Decoder<kfstack> = + Json.jIndex<'#kfstack'>('#kfstack'); + +/** Natural order for `kfstack` */ +export const byKfstack: Compare.Order<kfstack> = Compare.number; + +/** Default value for `kfstack` */ +export const kfstackDefault: kfstack = + Json.jIndex<'#kfstack'>('#kfstack')(-1); + +/** Kernel function list infos */ +export type calllink = { callee: decl, caller?: decl }; + +/** Decoder for `calllink` */ +export const jCalllink: Json.Decoder<calllink> = + Json.jObject({ callee: jDecl, caller: Json.jOption(jDecl),}); + +/** Natural order for `calllink` */ +export const byCalllink: Compare.Order<calllink> = + Compare.byFields + <{ callee: decl, caller?: decl }>({ + callee: byDecl, + caller: Compare.defined(byDecl), + }); + +/** Default value for `calllink` */ +export const calllinkDefault: calllink = + { callee: declDefault, caller: undefined }; + +/** Data for array rows [`evaFlamegraph`](#evaflamegraph) */ +export interface evaFlamegraphData { + /** Entry identifier. */ + key: Json.key<'#evaFlamegraph'>; + /** Caller list identifier */ + stack: kfstack; + /** Computation time for the kernel function stack */ + time: number; + /** Kernel function description */ + title: string; + /** Function name */ + name: string; + /** Function list */ + funlist: string; + /** Kernel function key */ + kfkey: string; +} + +/** Decoder for `evaFlamegraphData` */ +export const jEvaFlamegraphData: Json.Decoder<evaFlamegraphData> = + Json.jObject({ + key: Json.jKey<'#evaFlamegraph'>('#evaFlamegraph'), + stack: jKfstack, + time: Json.jNumber, + title: Json.jString, + name: Json.jString, + funlist: Json.jString, + kfkey: Json.jString, + }); + +/** Natural order for `evaFlamegraphData` */ +export const byEvaFlamegraphData: Compare.Order<evaFlamegraphData> = + Compare.byFields + <{ key: Json.key<'#evaFlamegraph'>, stack: kfstack, time: number, + title: string, name: string, funlist: string, kfkey: string }>({ + key: Compare.string, + stack: byKfstack, + time: Compare.number, + title: Compare.string, + name: Compare.string, + funlist: Compare.string, + kfkey: Compare.string, + }); + +/** Signal for array [`evaFlamegraph`](#evaflamegraph) */ +export const signalEvaFlamegraph: Server.Signal = { + name: 'plugins.eva.general.signalEvaFlamegraph', +}; + +const reloadEvaFlamegraph_internal: Server.GetRequest<null,null> = { + kind: Server.RqKind.GET, + name: 'plugins.eva.general.reloadEvaFlamegraph', + input: Json.jNull, + output: Json.jNull, + fallback: null, + signals: [], +}; +/** Force full reload for array [`evaFlamegraph`](#evaflamegraph) */ +export const reloadEvaFlamegraph: Server.GetRequest<null,null>= reloadEvaFlamegraph_internal; + +const fetchEvaFlamegraph_internal: Server.GetRequest< + number, + { reload: boolean, removed: Json.key<'#evaFlamegraph'>[], + updated: evaFlamegraphData[], pending: number } + > = { + kind: Server.RqKind.GET, + name: 'plugins.eva.general.fetchEvaFlamegraph', + input: Json.jNumber, + output: Json.jObject({ + reload: Json.jBoolean, + removed: Json.jArray( + Json.jKey<'#evaFlamegraph'>('#evaFlamegraph')), + updated: Json.jArray(jEvaFlamegraphData), + pending: Json.jNumber, + }), + fallback: { reload: false, removed: [], updated: [], pending: 0 }, + signals: [], +}; +/** Data fetcher for array [`evaFlamegraph`](#evaflamegraph) */ +export const fetchEvaFlamegraph: Server.GetRequest< + number, + { reload: boolean, removed: Json.key<'#evaFlamegraph'>[], + updated: evaFlamegraphData[], pending: number } + >= fetchEvaFlamegraph_internal; + +const evaFlamegraph_internal: State.Array< + Json.key<'#evaFlamegraph'>, + evaFlamegraphData + > = { + name: 'plugins.eva.general.evaFlamegraph', + getkey: ((d:evaFlamegraphData) => d.key), + signal: signalEvaFlamegraph, + fetch: fetchEvaFlamegraph, + reload: reloadEvaFlamegraph, + order: byEvaFlamegraphData, +}; +/** Data for Eva flamegraph */ +export const evaFlamegraph: State.Array< + Json.key<'#evaFlamegraph'>, + evaFlamegraphData + > = evaFlamegraph_internal; + +/** Default value for `evaFlamegraphData` */ +export const evaFlamegraphDataDefault: evaFlamegraphData = + { key: Json.jKey<'#evaFlamegraph'>('#evaFlamegraph')(''), + stack: kfstackDefault, time: 0, title: '', name: '', funlist: '', + kfkey: '' }; + /* ------------------------------------- */ diff --git a/ivette/src/frama-c/plugins/eva/index.tsx b/ivette/src/frama-c/plugins/eva/index.tsx index d277a0fe8d7446e85312ffb17e8d4534b23c8a03..18d19940d089dcd90c2177ed3ebf2cd37cc96706 100644 --- a/ivette/src/frama-c/plugins/eva/index.tsx +++ b/ivette/src/frama-c/plugins/eva/index.tsx @@ -30,6 +30,7 @@ import './Summary'; import './Coverage'; import './DomainStates'; import './EvaSidebar'; +import './Flamegraph'; import './style.css'; // -------------------------------------------------------------------------- @@ -47,7 +48,8 @@ Ivette.registerView({ layout: { 'A': 'fc.eva.summary', 'B': 'fc.eva.coverage', - 'CD': 'fc.kernel.messages', + 'C': 'fc.kernel.messages', + 'D': 'fc.eva.flamegraph', }, }); diff --git a/ivette/src/frama-c/plugins/eva/style.css b/ivette/src/frama-c/plugins/eva/style.css index 41fb3879bf8948076ed5433ae54c9352183ae3ab..3b7861ae8effa376b882369b1e46db2f6c1b25b2 100644 --- a/ivette/src/frama-c/plugins/eva/style.css +++ b/ivette/src/frama-c/plugins/eva/style.css @@ -339,6 +339,17 @@ tr:first-of-type > .eva-table-callsite-box { .eva-status-icon.eva-computed { fill: var(--eva-alarms-true); } .eva-status-icon.eva-not_computed { fill: var(--eva-alarms-false); } +/* -------------------------------------------------------------------------- */ +/* --- Flamegraph --- */ +/* -------------------------------------------------------------------------- */ + +.flame-details { + padding: 3px 10px; + position: absolute; + bottom : 0; + background-color: var(--background-report); +} + /* -------------------------------------------------------------------------- */ /* --- Sidebar Eva --- */ /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/frama-c/react-flame-graph.d.ts b/ivette/src/frama-c/react-flame-graph.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff0062665305dbcce1dea513f2bd7856c6b4e3bc --- /dev/null +++ b/ivette/src/frama-c/react-flame-graph.d.ts @@ -0,0 +1,23 @@ +/* ************************************************************************ */ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2024 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/* ************************************************************************ */ + +declare module 'react-flame-graph'; diff --git a/ivette/yarn.lock b/ivette/yarn.lock index 573ecf8a41acd737c6ebc929ed39fee2dce137db..2f199e3035293664ca748c61734b6ac3db1d2198 100644 --- a/ivette/yarn.lock +++ b/ivette/yarn.lock @@ -3223,6 +3223,11 @@ flatted@^3.1.0: resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== +flow-bin@^0.118.0: + version "0.118.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.118.0.tgz#fb706364a58c682d67a2ca7df39396467dc397d1" + integrity sha512-jlbUu0XkbpXeXhan5xyTqVK1jmEKNxE8hpzznI3TThHTr76GiFwK0iRzhDo4KNy+S9h/KxHaqVhTP86vA6wHCg== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" @@ -4350,6 +4355,16 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" +"memoize-one@>=3.1.1 <6": + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +memoize-one@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" + integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" @@ -4994,6 +5009,15 @@ react-fast-compare@^3.2.2: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== +react-flame-graph@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/react-flame-graph/-/react-flame-graph-1.4.0.tgz#52d118cc94348f630a812fc0ec530a5b73c30cdb" + integrity sha512-DaCK9ZX+xK0mNca72kUE5cu6T8hGe/KLsefQWf+eT9sVt+0WP1dVxZCGD8Svfn2KrZB9Mv011Intg/yG2YWSxA== + dependencies: + flow-bin "^0.118.0" + memoize-one "^3.1.1" + react-window "^1" + react-force-graph-2d@^1.25.4: version "1.25.4" resolved "https://registry.yarnpkg.com/react-force-graph-2d/-/react-force-graph-2d-1.25.4.tgz#91f9e8169d0eeb6a7e36c36dd99da5128702b776" @@ -5077,6 +5101,14 @@ react-virtualized@9.22.5: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" +react-window@^1: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^18: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" diff --git a/src/plugins/eva/api/general_requests.ml b/src/plugins/eva/api/general_requests.ml index a6ba38f5c9fd9a5c423ac7d9489305bcbb884d8c..afcf9ac7c2036366833b2bdd636faeb4ee5dd3c4 100644 --- a/src/plugins/eva/api/general_requests.ml +++ b/src/plugins/eva/api/general_requests.ml @@ -868,3 +868,98 @@ let () = Request.register ~package (Data.Jtriple (Data.Jstring) (Data.Jstring) (Data.Jstring))) ~signals:[computation_signal] get_states + + + +(* ----- Flamegraph --------------------------------------------------------- *) + +module Jkfstack : +sig + include Data.S with type t = Eva_perf.KfList.t + val get : Eva_perf.KfList.t -> int +end = Data.Index + (Eva_perf.KfList.Map) + (struct + let package = package + let name = "kfstack" + let descr = Markdown.plain "Kernel function stack identifier" + end) + +module Jkfs : Request.Output with type t = Eva_perf.KfList.t = struct + + type t = Eva_perf.KfList.t + + let jcalllink = Server.Data.declare ~package + ~name:"calllink" ~descr:(Markdown.plain "Kernel function list infos") + (Jrecord [ + "callee" , Kernel_ast.Decl.jtype ; + "caller" , Joption Kernel_ast.Decl.jtype ; + ]) + + let jtype = Package.(Jarray jcalllink) + + let jkfstack ~jcaller ~jcallee = + `Assoc [ + "callee", jcallee ; + "caller", jcaller ; + ] + + let to_json (cl : t) = + let aux (acc, jcaller) callee = + let jcallee = Kernel_ast.Decl.to_json (SFunction callee) in + jkfstack ~jcaller ~jcallee :: acc, jcallee + in + match cl with + | [] -> `List [] + | entry :: r -> + let entry_point = Kernel_ast.Decl.to_json (SFunction entry) in + let l, _last_callee = + List.fold_left aux + ([`Assoc [ "callee", entry_point ]], entry_point) + (List.rev r) + in `List l + +end + +let _evaFlamegraph = + let model = States.model () in + (* This field is useful for interacting with other components, + eg. the currently selected callstack in EVA values *) + States.column model ~name:"stack" + ~descr:(Markdown.plain "Caller list identifier") + ~data:(module Jkfstack) ~get:fst ; + (* This field contains the computation time *) + States.column model ~name:"time" + ~descr:(Markdown.plain "Computation time for the kernel function stack") + ~data:(module Data.Jfloat) + ~get:(fun (_cs, (_start, duration)) -> duration); + (* This field might be useful to display tooltips on the flames *) + States.column model ~name:"title" + ~descr:(Markdown.plain "Kernel function description") + ~data:(module Data.Jstring) + ~get:(fun (cl,_) -> Pretty_utils.to_string Eva_perf.KfList.pretty cl); + (* This field contains the name of the function on top of the + kernel function stack *) + States.column model ~name:"name" + ~descr:(Markdown.plain "Function name") + ~data:(module Data.Jstring) + ~get:(fun (cl,_) -> Kernel_function.get_name (List.(hd (rev cl)))); + (* This field contains the list of the function names *) + States.column model ~name:"funlist" + ~descr:(Markdown.plain "Function list") + ~data:(module Data.Jstring) + ~get:(fun (cl,_) -> + Pretty_utils.to_string (Eva_perf.KfList.pretty ~sep:":") cl); + (* This field contains the declaration of the function on top of the + kernel function stack *) + States.column model ~name:"kfkey" + ~descr:(Markdown.plain "Kernel function key") + ~data:(module Data.Jstring) + ~get:(fun (cl,_) -> Kernel_ast.Decl.index (SFunction (List.(hd (rev cl))))); + (* Add/remove other fields if necessary... *) + States.register_framac_array + ~package + ~name:"evaFlamegraph" + ~descr:(Markdown.plain "Data for Eva flamegraph") + ~key:(fun cl -> Format.sprintf "#%06d" @@ Jkfstack.get cl) + model (module Eva_perf.EvaFlamegraph) diff --git a/src/plugins/eva/utils/eva_perf.ml b/src/plugins/eva/utils/eva_perf.ml index efb4a27305ab0ab8bd4c7af35ec92b903fb956a9..8486714a22a6f306595cca4741db4e6328497a3f 100644 --- a/src/plugins/eva/utils/eva_perf.ml +++ b/src/plugins/eva/utils/eva_perf.ml @@ -353,110 +353,116 @@ let reset_perf () = (* Set to [Some _] if option [-eva-flamegraph] is set and [main] is currently being analyzed and the file is ok. Otherwise, set to [None]. *) -let oc_flamegraph = ref None - -let stack_flamegraph = ref [] -(* Callstack for flamegraphs. The most recent function is at the top of the - list. The elements of the list are [(starting_time, self_total_time)]. - [starting_time] is the time when we started analyzing the function. - [total_time] is the time spent so far in the function itself, _without the - callees_. [total_time] is updated from [starting_time] when we start a - callee, or when the analysis of the function ends. This stack is never - empty when an analysis is in progress. *) - -(* pretty-prints the functions in a Value callstack, starting by main (i.e. - in reverse order). *) -let pretty_callstack oc callstack = - let rec aux oc = function - | [] -> () (* does not happen in theory *) - | [main] -> Printf.fprintf oc "%s" (Kernel_function.get_name main) - | kf :: q -> Printf.fprintf oc "%s;%a" (Kernel_function.get_name kf) aux q - in - aux oc (Callstack.to_kf_list callstack) +let oc_fmt_flamegraph = ref None + +(* We cannot use a Callstack here because we ignore the call statements. *) +module KfList = struct + include Datatype.List_with_collections(Cil_datatype.Kf) + (struct + let module_name = "Eva.KfList" + end) + let pretty ?(sep=format_of_string ";") fmt l = + Pretty_utils.pp_flowlist ~left:"" ~sep ~right:"" + (fun fmt kf -> Kernel_function.pretty fmt kf) + fmt l +end + +(* A mapping from callstacks to (time_of_last_call, total_duration). + [time_of_last_call] is the timestamp of the last time that the leaf function + in the callstack was called. + [total_duration] is the accumulated total amount of time spent in the + callstack. +*) +module EvaFlamegraph = + State_builder.Hashtbl + (KfList.Hashtbl) + (Datatype.Pair(Datatype.Float)(Datatype.Float)) + (struct + let name = "Eva.Flamegraph" + let dependencies = [ Ast.self ] + let size = 20 + end) (* update the [self_total_time] information for the function being analyzed, assuming that the current time is [time] *) -let update_self_total_time time = - match !stack_flamegraph with - | [] -> assert false - | (start_caller, total) :: q -> +let update_self_total_time cl time = + try + let (start_caller, total) = EvaFlamegraph.find cl in let d = duration start_caller time in - stack_flamegraph := (start_caller, d +. total) :: q + let new_total = d +. total in + EvaFlamegraph.replace cl (time, new_total); + new_total + with Not_found -> + Self.fatal + "EvaFlamegraph: caller list not found: %a" + (KfList.pretty ~sep:";") cl + +let start_call_flamegraph_main cl = + (* Analysis of main *) + EvaFlamegraph.clear (); + EvaFlamegraph.add cl (Sys.time (), 0.0); + if not (Parameters.ValPerfFlamegraphs.is_empty ()) then begin + let file = Parameters.ValPerfFlamegraphs.get () in + try + let oc = open_out (file:>string) in + oc_fmt_flamegraph := Some (oc, Format.formatter_of_out_channel oc); + with e -> + Self.error "cannot open flamegraph file: %s" + (Printexc.to_string e); + oc_fmt_flamegraph := None (* to be on the safe side *) + end (* called when a new function is being analyzed *) -let start_doing_flamegraph callstack = - match callstack.Callstack.stack with - | [] -> - (* Analysis of main *) - if not (Parameters.ValPerfFlamegraphs.is_empty ()) then begin - let file = Parameters.ValPerfFlamegraphs.get () in - try - (* Flamegraphs must be computed. Set up the stack and the output file *) - let oc = open_out (file:>string) in - oc_flamegraph := Some oc; - stack_flamegraph := [ (Sys.time (), 0.) ] - with e -> - Self.error "cannot open flamegraph file: %s" - (Printexc.to_string e); - oc_flamegraph := None (* to be on the safe side *) - end - | _ :: _ -> - if !oc_flamegraph <> None then - (* Flamegraphs are being computed. Update time spent in current function - so far, then push a slot for the analysis of the new function *) - let time = Sys.time () in - update_self_total_time time; - stack_flamegraph := (time, 0.) :: !stack_flamegraph; +let start_call_flamegraph ~prev cs = + let cl = Callstack.to_kf_list cs in + let prev = Option.map Callstack.to_kf_list prev in + match prev with + | None -> start_call_flamegraph_main cl + | Some prev -> + let time = Sys.time () in + ignore (update_self_total_time prev time); + let (_start, total) = + try EvaFlamegraph.find cl with Not_found -> (0.0, 0.0) + in + EvaFlamegraph.replace cl (time, total) ;; (* called when the analysis of a function ends. This function is at the top of [callstack] *) -let stop_doing_flamegraph callstack = - match !oc_flamegraph with - | None -> () - | Some oc -> (* Flamegraphs are being recorded *) - let time = Sys.time() in - update_self_total_time time; (* update current function *) - match !stack_flamegraph with - | [] -> assert false - | (_, total) :: q -> - (* dump the total time (that we just updated) for the current function *) - Printf.fprintf oc "%a %.3f\n%!" - pretty_callstack callstack (total *. 1000.); - match q with - | [] -> stack_flamegraph := [] (* we are back to the main function *) - | (_, total_caller) :: q' -> - (* drop the current function from the flamegraph stack AND update - the 'current time' information, so that the time spent in the - callee is not counted. *) - stack_flamegraph := (time, total_caller) :: q' -;; +let end_call_flamegraph cs = + let cl = Callstack.to_kf_list cs in + let time = Sys.time () in + let total = update_self_total_time cl time in (* update current function *) + begin + match !oc_fmt_flamegraph with + | None -> () + | Some (_, fmt) -> (* Flamegraphs are being written to a file *) + Format.fprintf fmt "%a %.3f\n%!" + (KfList.pretty ~sep:";") cl (total *. 1000.) + end let reset_flamegraph () = - match !oc_flamegraph with + EvaFlamegraph.clear (); + match !oc_fmt_flamegraph with | None -> () - | Some fd -> close_out fd; stack_flamegraph := []; oc_flamegraph := None - + | Some (oc, _) -> + close_out oc; oc_fmt_flamegraph := None (* -------------------------------------------------------------------------- *) (* --- Exported interface --- *) (* -------------------------------------------------------------------------- *) -let start_doing callgraph = - start_doing_perf callgraph; - start_doing_flamegraph callgraph; -;; +let start_call ~prev cs = + start_doing_perf cs; + start_call_flamegraph ~prev cs -let stop_doing callgraph = +let end_call callgraph = stop_doing_perf callgraph; - stop_doing_flamegraph callgraph; -;; - + end_call_flamegraph callgraph let reset () = reset_perf (); - reset_flamegraph (); -;; + reset_flamegraph () (* TODO: Output files with more graphical outputs, such as diff --git a/src/plugins/eva/utils/eva_perf.mli b/src/plugins/eva/utils/eva_perf.mli index 77bfd55f02a30fc6758f6f7ed3a180ecaf8cde43..20a78d8f2586145482bded1d76791a6f3c1f2f99 100644 --- a/src/plugins/eva/utils/eva_perf.mli +++ b/src/plugins/eva/utils/eva_perf.mli @@ -20,13 +20,13 @@ (* *) (**************************************************************************) -(** Call [start_doing] when starting analyzing a new function. The new - function is on the top of the call stack.*) -val start_doing: Callstack.t -> unit +(** Call [start_call] when starting analyzing a new function call. + The new function is on the top of the call stack.*) +val start_call: prev:Callstack.t option -> Callstack.t -> unit -(** Call [start_doing] when finishing analyzing a function. The +(** Call [end_call] when finishing analyzing a function. The function must still be on the top of the call stack. *) -val stop_doing: Callstack.t -> unit +val end_call: Callstack.t -> unit (** Display a complete summary of performance informations. Can be called during the analysis. *) @@ -35,3 +35,13 @@ val display: Format.formatter -> unit (** Reset the internal state of the module; to call at the very beginning of the analysis. *) val reset: unit -> unit + +module KfList : sig + include Datatype.S_with_collections with type t = Kernel_function.t list + val pretty : ?sep:Pretty_utils.sformat -> Format.formatter -> + Kernel_function.t list -> unit +end + +module EvaFlamegraph : + State_builder.Hashtbl with type key = KfList.t + and type data = float * float diff --git a/src/plugins/eva/utils/eva_utils.ml b/src/plugins/eva/utils/eva_utils.ml index f8da531466ce7d087c4a37df41e232e67fa5fdb2..eafb7d948df2fca06a4cc9932f78d69d51e04f4d 100644 --- a/src/plugins/eva/utils/eva_utils.ml +++ b/src/plugins/eva/utils/eva_utils.ml @@ -30,14 +30,14 @@ let clear_call_stack () = match !current_callstack with | None -> () | Some cs -> - Eva_perf.stop_doing cs; + Eva_perf.end_call cs; current_callstack := None let init_call_stack kf = assert (!current_callstack = None); let cs = Callstack.init kf in current_callstack := Some cs; - Eva_perf.start_doing cs; + Eva_perf.start_call ~prev:None cs; cs let current_call_stack_opt () = !current_callstack @@ -55,11 +55,11 @@ let push_call_stack kf stmt = let cs = current_call_stack () in let new_cs = Callstack.push kf stmt cs in current_callstack := Some new_cs; - Eva_perf.start_doing new_cs + Eva_perf.start_call ~prev:(Some cs) new_cs let pop_call_stack () = let cs = current_call_stack () in - Eva_perf.stop_doing cs; + Eva_perf.end_call cs; current_callstack := Callstack.pop cs let pp_callstack fmt =