From e6e0cdc8cf07cc83daa4801840e29a26e36e2ee0 Mon Sep 17 00:00:00 2001 From: Valentin Perrelle <valentin.perrelle@cea.fr> Date: Fri, 31 Mar 2023 16:17:43 +0200 Subject: [PATCH] [Ivette] Displays the focused callstack --- .../frama-c/plugins/callgraph/api/index.ts | 36 +++++------ .../plugins/callgraph/graph-style.json | 8 +++ .../src/frama-c/plugins/callgraph/index.tsx | 60 +++++++++++++++---- ivette/src/frama-c/plugins/eva/valuetable.tsx | 2 +- src/plugins/callgraph/callgraph_api.ml | 5 +- src/plugins/callgraph/cg.ml | 6 +- src/plugins/callgraph/requests.ml | 50 +++++++++------- 7 files changed, 110 insertions(+), 57 deletions(-) diff --git a/ivette/src/frama-c/plugins/callgraph/api/index.ts b/ivette/src/frama-c/plugins/callgraph/api/index.ts index af0fd6b1f54..e231a66fbba 100644 --- a/ivette/src/frama-c/plugins/callgraph/api/index.ts +++ b/ivette/src/frama-c/plugins/callgraph/api/index.ts @@ -55,34 +55,38 @@ import { tag } from 'frama-c/kernel/api/data'; import { tagDefault } from 'frama-c/kernel/api/data'; export interface vertex { - /** kf */ + /** The function represented by the node */ kf: fct; - /** is_root */ + /** whether this node is the root of a service */ is_root: boolean; + /** the root of this node's service */ + root: fct; } /** Decoder for `vertex` */ export const jVertex: Json.Decoder<vertex> = - Json.jObject({ kf: jFct, is_root: Json.jBoolean,}); + Json.jObject({ kf: jFct, is_root: Json.jBoolean, root: jFct,}); /** Natural order for `vertex` */ export const byVertex: Compare.Order<vertex> = Compare.byFields - <{ kf: fct, is_root: boolean }>({ + <{ kf: fct, is_root: boolean, root: fct }>({ kf: byFct, is_root: Compare.boolean, + root: byFct, }); /** Default value for `vertex` */ -export const vertexDefault: vertex = { kf: fctDefault, is_root: false }; +export const vertexDefault: vertex = + { kf: fctDefault, is_root: false, root: fctDefault }; -/** Whether The nature of a node */ +/** Whether the call goes through services or not */ export enum edgeKind { - /** */ + /** a call between two services */ inter_services = 'inter_services', - /** */ + /** a call inside a service */ inter_functions = 'inter_functions', - /** */ + /** both cases above */ both = 'both', } @@ -137,30 +141,22 @@ export interface graph { vertices: vertex[]; /** edges */ edges: edge[]; - /** entry_point */ - entry_point?: fct; } /** Decoder for `graph` */ export const jGraph: Json.Decoder<graph> = - Json.jObject({ - vertices: Json.jArray(jVertex), - edges: Json.jArray(jEdge), - entry_point: Json.jOption(jFct), - }); + Json.jObject({ vertices: Json.jArray(jVertex), edges: Json.jArray(jEdge),}); /** Natural order for `graph` */ export const byGraph: Compare.Order<graph> = Compare.byFields - <{ vertices: vertex[], edges: edge[], entry_point?: fct }>({ + <{ vertices: vertex[], edges: edge[] }>({ vertices: Compare.array(byVertex), edges: Compare.array(byEdge), - entry_point: Compare.defined(byFct), }); /** Default value for `graph` */ -export const graphDefault: graph = - { vertices: [], edges: [], entry_point: undefined }; +export const graphDefault: graph = { vertices: [], edges: [] }; /** Signal for state [`callgraph`](#callgraph) */ export const signalCallgraph: Server.Signal = { diff --git a/ivette/src/frama-c/plugins/callgraph/graph-style.json b/ivette/src/frama-c/plugins/callgraph/graph-style.json index 6a10b6d0882..e1aff869e93 100644 --- a/ivette/src/frama-c/plugins/callgraph/graph-style.json +++ b/ivette/src/frama-c/plugins/callgraph/graph-style.json @@ -30,5 +30,13 @@ "target-arrow-color": "#888", "arrow-scale": 2.0 } + }, + { + "selector": ".callstack-selected", + "style": { + "overlay-color": "#8bf", + "overlay-padding": "8px", + "overlay-opacity": 0.4 + } } ] diff --git a/ivette/src/frama-c/plugins/callgraph/index.tsx b/ivette/src/frama-c/plugins/callgraph/index.tsx index e5c8ccfc9d9..73cddb54911 100644 --- a/ivette/src/frama-c/plugins/callgraph/index.tsx +++ b/ivette/src/frama-c/plugins/callgraph/index.tsx @@ -20,22 +20,27 @@ /* */ /* ************************************************************************ */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import _ from 'lodash'; import * as Ivette from 'ivette'; import * as Server from 'frama-c/server'; -import * as API from './api'; +import * as AstAPI from 'frama-c/kernel/api/ast'; +import * as CgAPI from './api'; +import * as ValuesAPI from 'frama-c/plugins/eva/api/values'; -import Cytoscape from 'cytoscape'; +import Cy from 'cytoscape'; import CytoscapeComponent from 'react-cytoscapejs'; import 'frama-c/plugins/dive/cytoscape_libs'; import 'cytoscape-panzoom/cytoscape.js-panzoom.css'; - import style from './graph-style.json'; -import { useSyncValue } from 'frama-c/states'; + +import { useGlobalState } from 'dome/data/states'; +import { useRequest, useSyncValue } from 'frama-c/states'; import gearsIcon from 'frama-c/plugins/eva/images/gears.svg'; +import { CallstackState } from 'frama-c/plugins/eva/valuetable'; + import './callgraph.css'; @@ -73,27 +78,56 @@ function getWidth(node: any): string { // --- Graph // -------------------------------------------------------------------------- -function convertGraph(graph: API.graph): object[] { +function edgeId(source: AstAPI.fct, target: AstAPI.fct): string { + return `${source}-${target}`; +} + +function convertGraph(graph: CgAPI.graph): object[] { const elements = []; for (const v of graph.vertices) { - elements.push({data: {...v, id:v.kf}}); + elements.push({data: {...v, id: v.kf}}); } for (const e of graph.edges) { - elements.push({data: {source: e.src, target: e.dst}}); + const id = edgeId(e.src, e.dst); + elements.push({data: {...e, id, source: e.src, target: e.dst}}); } - console.log(elements); return elements; } +type callstack = { + callee: AstAPI.fct, + caller?: AstAPI.fct, + stmt?: AstAPI.marker, + rank?: number +}[] + +function selectCallstack(cy: Cy.Core, callstack: callstack | undefined): void { + const className = 'callstack-selected'; + cy.$(`.${className}`).removeClass(className); + callstack?.forEach((call) => { + cy.$(`node[id='${call.callee}']`).addClass(className); + if (call.caller) { + const id = edgeId(call.caller, call.callee); + cy.$(`edge[id='${id}']`).addClass(className); + } + }); +} function Callgraph() : JSX.Element { - const isComputed = useSyncValue(API.isComputed); - const graph = useSyncValue(API.callgraph); - const [cy, setCy] = useState<Cytoscape.Core>(); + const isComputed = useSyncValue(CgAPI.isComputed); + const graph = useSyncValue(CgAPI.callgraph); + const [cy, setCy] = useState<Cy.Core>(); + const [cs] = useGlobalState(CallstackState); + const callstack = useRequest(ValuesAPI.getCallstackInfo, cs); + const layout = {name: 'cola', nodeSpacing: 32}; + useEffect(() => { + cy && selectCallstack(cy, callstack); + }, [cy, callstack]); + if (isComputed === false) { - Server.send(API.compute, null); + Server.send(CgAPI.compute, null); return (<img src={gearsIcon} className="callgraph-computing" />); } else if (graph !== undefined) { diff --git a/ivette/src/frama-c/plugins/eva/valuetable.tsx b/ivette/src/frama-c/plugins/eva/valuetable.tsx index 2b2490d51ad..95a83acbc70 100644 --- a/ivette/src/frama-c/plugins/eva/valuetable.tsx +++ b/ivette/src/frama-c/plugins/eva/valuetable.tsx @@ -972,7 +972,7 @@ function useEvaluationMode(props: EvaluationModeProps): void { /* -------------------------------------------------------------------------- */ /* Table's state. It is global for when the user changes the view. */ -const CallstackState = new GlobalState<callstack>('Summary'); +export const CallstackState = new GlobalState<callstack>('Summary'); const FunctionsManagerState = new GlobalState(new FunctionsManager()); const FocusState = new GlobalState<Probe | undefined>(undefined); diff --git a/src/plugins/callgraph/callgraph_api.ml b/src/plugins/callgraph/callgraph_api.ml index a2d67c27747..8ae2e7c7fca 100644 --- a/src/plugins/callgraph/callgraph_api.ml +++ b/src/plugins/callgraph/callgraph_api.ml @@ -43,6 +43,9 @@ module type Graph = sig val is_computed: unit -> bool (** Is the graph already built? *) + val add_hook: (G.t -> unit) -> unit + (** Call registered hook each time the graph is computed *) + val self: State.t end @@ -60,8 +63,6 @@ module type Services = sig val entry_point: unit -> G.V.t option val is_root: Kernel_function.t -> bool - - val add_hook: (G.t -> unit) -> unit end (* diff --git a/src/plugins/callgraph/cg.ml b/src/plugins/callgraph/cg.ml index 24c12d9a2ea..1d081686740 100644 --- a/src/plugins/callgraph/cg.ml +++ b/src/plugins/callgraph/cg.ml @@ -66,8 +66,11 @@ module State = let dependencies = [ Eva.Analysis.self; Globals.Functions.self ] end) +module StateHook = Hook.Build (D) + let self = State.self let is_computed () = State.is_computed () +let add_hook = StateHook.extend (** @return the list of functions which address is taken.*) let get_pointed_kfs = @@ -101,7 +104,6 @@ let get_pointed_kfs = match !res with | None -> let l = compute () in - State.mark_as_computed (); res := Some l; l | Some l -> l @@ -232,6 +234,8 @@ let compute () = semantic_compute g end else (if Eva.Analysis.is_computed () then semantic_compute else syntactic_compute) g; + State.mark_as_computed (); + StateHook.apply g; g let get () = State.memo compute diff --git a/src/plugins/callgraph/requests.ml b/src/plugins/callgraph/requests.ml index 42db7a20ad8..cdef7f503d1 100644 --- a/src/plugins/callgraph/requests.ml +++ b/src/plugins/callgraph/requests.ml @@ -22,8 +22,15 @@ open Server +module G = Services.G + +(* --- Package declaration --- *) + let package = Package.package ~plugin:"callgraph" ~title:"Callgraph" () + +(* --- Helper modules --- *) + module Record () = struct module Record = Data.Record @@ -31,14 +38,11 @@ struct let record : record Record.signature = Record.signature () let field name ?(descr = name) = Record.field record ~name ~descr:(Markdown.plain descr) - let option name ?(descr = name) = - Record.option record ~name ~descr:(Markdown.plain descr) let publish ?descr name = let descr = Option.map Markdown.plain descr in Record.publish record ~package ~name ?descr end - module Enum (X: sig type t end) = struct module Enum = Data.Enum @@ -49,12 +53,19 @@ struct Request.dictionary ~package ~name ~descr:(Markdown.plain descr) dictionary end + +(* --- Types --- *) + module Vertex = struct include Record () let kf = field "kf" (module Kernel_ast.Function) + ~descr: "The function represented by the node" let is_root = field "is_root" Data.jbool + ~descr: "whether this node is the root of a service" + let root = field "root" (module Kernel_ast.Function) + ~descr: "the root of this node's service" include (val publish "vertex") type t = Cil_types.kernel_function Service_graph.vertex @@ -63,6 +74,7 @@ struct default |> set kf v.node |> set is_root v.is_root |> + set root v.root.node |> to_json let of_json _js = Data.failure "Vertex.of_json not implemented" @@ -72,16 +84,17 @@ module EdgeKind = struct include Enum (struct type t = Service_graph.edge end) - let inter_services = tag "inter_services" "" - let inter_functions = tag "inter_functions" "" - let both = tag "both" "" + let inter_services = tag "inter_services" "a call between two services" + let inter_functions = tag "inter_functions" "a call inside a service" + let both = tag "both" "both cases above" let lookup = function | Service_graph.Inter_services -> inter_services | Inter_functions -> inter_functions | Both -> both - include (val publish lookup "edgeKind" "Whether The nature of a node") + include (val publish lookup + "edgeKind" "Whether the call goes through services or not") end module Edge = @@ -93,13 +106,13 @@ struct let kind = field "kind" (module EdgeKind) include (val publish "edge") - type t = Services.G.E.t + type t = G.E.t let to_json (e : t) = default |> - set src (Services.G.E.src e).node |> - set dst (Services.G.E.dst e).node |> - set kind (Services.G.E.label e) |> + set src (G.E.src e).node |> + set dst (G.E.dst e).node |> + set kind (G.E.label e) |> to_json let of_json _js = Data.failure "Edge.of_json not implemented" @@ -111,31 +124,28 @@ struct let vertices = field "vertices" (module Data.Jlist (Vertex)) let edges = field "edges" (module Data.Jlist (Edge)) - let entry_point = option "entry_point" (module Kernel_ast.Function) include (val publish "graph" ~descr:"The callgraph of the current project") - type t = Services.G.t + type t = G.t let get_vertices (g : t) = - Services.G.fold_vertex (fun v acc -> v :: acc ) g [] + G.fold_vertex (fun v acc -> v :: acc ) g [] let get_edges (g : t) = - Services.G.fold_edges_e (fun v acc -> v :: acc ) g [] - - let get_entry_point () = - Services.entry_point () |> - Option.map (fun v -> v.Service_graph.node) + G.fold_edges_e (fun v acc -> v :: acc ) g [] let to_json (g : t) = default |> set vertices (get_vertices g) |> set edges (get_edges g) |> - set entry_point (get_entry_point ()) |> to_json let of_json _js = Data.failure "Graph.of_json not implemented" end + +(* --- Requests --- *) + let _signal = States.register_value ~package ~name:"callgraph" -- GitLab