From c3fc2b81489bfc6145d8609f5011e9e52e4bbb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr> Date: Wed, 16 Dec 2020 12:37:17 +0100 Subject: [PATCH] [ivette/eva] EVA value cache & sizing --- .../api/generated/plugins/eva/values/index.ts | 38 +++-- ivette/src/frama-c/eva/Values.tsx | 5 +- ivette/src/frama-c/eva/cells.ts | 139 +++++++++++++++++- ivette/src/frama-c/eva/layout.ts | 17 ++- ivette/src/frama-c/eva/model.ts | 15 +- ivette/src/frama-c/eva/probes.ts | 14 +- src/plugins/value/api/values_request.ml | 16 +- 7 files changed, 202 insertions(+), 42 deletions(-) diff --git a/ivette/api/generated/plugins/eva/values/index.ts b/ivette/api/generated/plugins/eva/values/index.ts index 605a73fcd98..5e4139969a3 100644 --- a/ivette/api/generated/plugins/eva/values/index.ts +++ b/ivette/api/generated/plugins/eva/values/index.ts @@ -29,30 +29,41 @@ export const changed: Server.Signal = { name: 'plugins.eva.values.changed', }; +export type callstack = Json.index<'#eva-callstack-id'>; + +/** Loose decoder for `callstack` */ +export const jCallstack: Json.Loose<callstack> = + Json.jIndex<'#eva-callstack-id'>('#eva-callstack-id'); + +/** Safe decoder for `callstack` */ +export const jCallstackSafe: Json.Safe<callstack> = + Json.jFail(Json.jIndex<'#eva-callstack-id'>('#eva-callstack-id'), + '#eva-callstack-id expected'); + +/** Natural order for `callstack` */ +export const byCallstack: Compare.Order<callstack> = Compare.number; + const getCallstacks_internal: Server.GetRequest< Json.key<'#stmt'>, - Json.index<'#eva-callstack-id'>[] + callstack[] > = { kind: Server.RqKind.GET, name: 'plugins.eva.values.getCallstacks', input: Json.jKey<'#stmt'>('#stmt'), - output: Json.jList(Json.jIndex<'#eva-callstack-id'>('#eva-callstack-id')), + output: Json.jList(jCallstack), }; /** Callstacks for markers */ -export const getCallstacks: Server.GetRequest< - Json.key<'#stmt'>, - Json.index<'#eva-callstack-id'>[] - >= getCallstacks_internal; +export const getCallstacks: Server.GetRequest<Json.key<'#stmt'>,callstack[]>= getCallstacks_internal; const getCallstackInfo_internal: Server.GetRequest< - Json.index<'#eva-callstack-id'>, + callstack, { calls: { fct: Json.key<'#fct'>, stmt?: Json.key<'#stmt'>, rank: number }[], descr: string } > = { kind: Server.RqKind.GET, name: 'plugins.eva.values.getCallstackInfo', - input: Json.jIndex<'#eva-callstack-id'>('#eva-callstack-id'), + input: jCallstack, output: Json.jObject({ calls: Json.jList( Json.jObject({ @@ -66,7 +77,7 @@ const getCallstackInfo_internal: Server.GetRequest< }; /** Callstack Description */ export const getCallstackInfo: Server.GetRequest< - Json.index<'#eva-callstack-id'>, + callstack, { calls: { fct: Json.key<'#fct'>, stmt?: Json.key<'#stmt'>, rank: number }[], descr: string } @@ -110,16 +121,13 @@ export const getProbeInfo: Server.GetRequest< >= getProbeInfo_internal; const getValues_internal: Server.GetRequest< - { callstacks?: Json.index<'#eva-callstack-id'>, target: marker }, + { callstack?: callstack, target: marker }, { v_else?: string, v_then?: string, v_after?: string, values?: string, alarms: [ "True" | "False" | "Unknown", string ][] } > = { kind: Server.RqKind.GET, name: 'plugins.eva.values.getValues', - input: Json.jObject({ - callstacks: Json.jIndex<'#eva-callstack-id'>('#eva-callstack-id'), - target: jMarkerSafe, - }), + input: Json.jObject({ callstack: jCallstack, target: jMarkerSafe,}), output: Json.jObject({ v_else: Json.jString, v_then: Json.jString, @@ -140,7 +148,7 @@ const getValues_internal: Server.GetRequest< }; /** Abstract values for the given marker */ export const getValues: Server.GetRequest< - { callstacks?: Json.index<'#eva-callstack-id'>, target: marker }, + { callstack?: callstack, target: marker }, { v_else?: string, v_then?: string, v_after?: string, values?: string, alarms: [ "True" | "False" | "Unknown", string ][] } >= getValues_internal; diff --git a/ivette/src/frama-c/eva/Values.tsx b/ivette/src/frama-c/eva/Values.tsx index e0d331efb9d..b9a984f3f43 100644 --- a/ivette/src/frama-c/eva/Values.tsx +++ b/ivette/src/frama-c/eva/Values.tsx @@ -19,6 +19,7 @@ import * as Server from 'frama-c/server'; import * as States from 'frama-c/states'; // Plugins +import * as Ast from 'frama-c/api/kernel/ast'; import * as Values from 'frama-c/api/plugins/eva/values'; // Locals @@ -284,8 +285,8 @@ function ValuesComponent() { Dome.useUpdate(ChangeEvent); Server.useSignal(Values.changed, forceUpdate); const [selection] = States.useSelection(); - const marker = selection?.current?.marker; - const probe = vstate.focus(marker); + const target = Ast.jMarker(selection?.current?.marker); + const probe = vstate.focus(target); const makeWindow = (size: Dimension) => ( <ValuesPanel vstate={vstate} {...size} /> ); diff --git a/ivette/src/frama-c/eva/cells.ts b/ivette/src/frama-c/eva/cells.ts index 062da8f5924..3611997bd13 100644 --- a/ivette/src/frama-c/eva/cells.ts +++ b/ivette/src/frama-c/eva/cells.ts @@ -1,5 +1,14 @@ // -------------------------------------------------------------------------- -// --- Cell Utilities & Value Cache +// --- Cells +// -------------------------------------------------------------------------- + +// Frama-C +import * as Server from 'frama-c/server'; +import * as Ast from 'frama-c/api/kernel/ast'; +import * as Values from 'frama-c/api/plugins/eva/values'; + +// -------------------------------------------------------------------------- +// --- Cell Utilities // -------------------------------------------------------------------------- export type callback = () => void; @@ -44,4 +53,132 @@ export function addV(a: Size, b: Size, padding = 0): Size { }; } +export function addS(s: Size, t: string | undefined): Size { + return t ? merge(s, sizeof(t)) : s; +} + +export function lt(a: Size, b: Size): boolean { + return a.rows < b.rows && a.cols < b.cols; +} + +export function leq(a: Size, b: Size): boolean { + return a.rows <= b.rows && a.cols <= b.cols; +} + +// -------------------------------------------------------------------------- +// --- Value Cache +// -------------------------------------------------------------------------- + +export type EvaStatus = 'True' | 'False' | 'Unknown'; +export type EvaAlarm = [EvaStatus, string]; + +export interface EvaValues { + errors?: string; + values?: string; + v_after?: string; + v_then?: string; + v_else?: string; + alarms?: EvaAlarm[]; + size: Size; +} + +export class ValueCache { + + private readonly state: StateCallbacks; + private readonly probes = new Map<Ast.marker, Size>(); // Marker -> max in column + private readonly stacks = new Map<Values.callstack, Size>(); // Callstack -> max in row + private readonly vcache = new Map<string, EvaValues>(); // '<Marker><@Callstack>?' -> value + private smax = EMPTY; // max cell size + + constructor(state: StateCallbacks) { + this.state = state; + } + + clear() { + this.smax = EMPTY; + this.probes.clear(); + this.stacks.clear(); + this.vcache.clear(); + this.state.forceLayout(); + } + + // --- Cached Measures + + getMaxSize() { return this.smax; } + + getProbeSize(target: Ast.marker) { + return this.probes.get(target) ?? EMPTY; + } + + getStackSize(callstack: Values.callstack) { + return this.stacks.get(callstack) ?? EMPTY; + } + + // --- Cached Values & Request Update + + getValues(target: Ast.marker, callstack?: Values.callstack): EvaValues { + const key = `${target}@${callstack ?? '*'}`; + const cache = this.vcache; + const cached = cache.get(key); + if (cached) return cached; + const newValue: EvaValues = { values: '', size: EMPTY }; + cache.set(key, newValue); + Server + .send(Values.getValues, { target, callstack }) + .then((r) => { + newValue.errors = undefined; + newValue.values = r.values; + newValue.v_after = r.v_after; + newValue.v_then = r.v_then; + newValue.v_else = r.v_else; + newValue.alarms = r.alarms; + if (this.updateLayout(target, callstack, newValue)) + this.state.forceLayout(); + else + this.state.forceUpdate(); + }) + .catch((err) => { + newValue.errors = `$Error: ${err}`; + this.state.forceUpdate(); + }); + return newValue; + } + + // --- Updating Measures + + private updateLayout( + target: Ast.marker, + callstack: Values.callstack | undefined, + v: EvaValues, + ): boolean { + // measuring cell + let s = sizeof(v.values); + s = addS(s, v.v_after); + s = addS(s, v.v_then); + s = addS(s, v.v_else); + v.size = s; + // max cell size + const { smax } = this; + let small = leq(s, smax); + if (!small) this.smax = merge(s, smax); + // max size for probe column + const ps = this.getProbeSize(target); + if (!leq(s, ps)) { + this.probes.set(target, merge(ps, s)); + small = false; + } + // max size for stack row + if (callstack) { + const cs = this.getStackSize(callstack); + if (!leq(s, cs)) { + this.stacks.set(callstack, merge(ps, s)); + small = false; + } + } + // request new layout if not small enough + return !small; + } + +} + // -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/eva/layout.ts b/ivette/src/frama-c/eva/layout.ts index 0b99c3cdc0a..2e54293d3e3 100644 --- a/ivette/src/frama-c/eva/layout.ts +++ b/ivette/src/frama-c/eva/layout.ts @@ -2,7 +2,7 @@ /* --- Layout ---*/ /* --------------------------------------------------------------------------*/ -import { Size, EMPTY, LABEL, addH } from './cells'; +import { Size, EMPTY, LABEL, addH, ValueCache } from './cells'; import { Probe } from './probes'; export interface LayoutProps { @@ -27,14 +27,16 @@ export class LayoutEngine { // --- Setup - /* private */ readonly wcrop: number; - /* private */ readonly hcrop: number; - /* private */ readonly margin: number; - /* private */ readonly remanent?: Probe; + private readonly cache: ValueCache; + private readonly wcrop: number; + private readonly hcrop: number; + private readonly margin: number; constructor( + cache: ValueCache, props: undefined | LayoutProps, ) { + this.cache = cache; const zoom = Math.max(0, props?.zoom ?? 0); this.hcrop = 1 + zoom; this.wcrop = LABEL + 2 * zoom; @@ -55,9 +57,10 @@ export class LayoutEngine { } push(p: Probe) { - const s = this.crop(p.summary); + const probeSize = this.cache.getProbeSize(p.marker); + const s = this.crop(probeSize); if (s.cols + this.rowSize.cols > this.margin) this.flush(); - p.layout = s; + p.colwidth = s.cols; this.rowSize = addH(this.rowSize, s); this.buffer.push(p); } diff --git a/ivette/src/frama-c/eva/model.ts b/ivette/src/frama-c/eva/model.ts index b05b6ec8b48..908c5315c48 100644 --- a/ivette/src/frama-c/eva/model.ts +++ b/ivette/src/frama-c/eva/model.ts @@ -6,9 +6,11 @@ import { throttle } from 'lodash'; import equal from 'react-fast-compare'; +import type { marker } from 'frama-c/api/kernel/ast'; + // Model -import { StateCallbacks, callback } from './cells'; import { Probe } from './probes'; +import { callback, StateCallbacks, ValueCache } from './cells'; import { LayoutProps, LayoutEngine, Row } from './layout'; /* --------------------------------------------------------------------------*/ @@ -29,11 +31,12 @@ export class Model implements StateCallbacks { } // --- Probes + private focused?: Probe; private remanent?: Probe; // last transient private probes = new Map<string, Probe>(); - getProbe(m: string): Probe { + getProbe(m: marker): Probe { let p = this.probes.get(m); if (!p) { p = new Probe(this, m); @@ -43,7 +46,7 @@ export class Model implements StateCallbacks { return p; } - focus(m: string | undefined): Probe | undefined { + focus(m: marker | undefined): Probe | undefined { const r = this.remanent; if (m) { const p = this.getProbe(m); @@ -62,6 +65,10 @@ export class Model implements StateCallbacks { return this.focused; } + // --- Values + + private readonly cache = new ValueCache(this); + // --- Rows private forcedLayout = false; @@ -83,7 +90,7 @@ export class Model implements StateCallbacks { toLayout.push(p); } }); - const engine = new LayoutEngine(this.layout); + const engine = new LayoutEngine(this.cache, this.layout); toLayout.sort(Probe.order).forEach(engine.push); this.rows = engine.flush(); this.forceUpdate(); diff --git a/ivette/src/frama-c/eva/probes.ts b/ivette/src/frama-c/eva/probes.ts index dfb327c8aa9..28f4a4f82a0 100644 --- a/ivette/src/frama-c/eva/probes.ts +++ b/ivette/src/frama-c/eva/probes.ts @@ -4,12 +4,11 @@ // Frama-C import * as Server from 'frama-c/server'; - -// Plugins import * as Values from 'frama-c/api/plugins/eva/values'; +import * as Ast from 'frama-c/api/kernel/ast'; // Model -import { StateCallbacks, Size, EMPTY, LABEL } from './cells'; +import { StateCallbacks, LABEL } from './cells'; /* --------------------------------------------------------------------------*/ /* --- Probe Labelling ---*/ @@ -43,21 +42,18 @@ function newLabel() { export class Probe { // properties - readonly marker: string; + readonly marker: Ast.marker; readonly state: StateCallbacks; transient = true; label?: string; code?: string; stmt?: string; rank?: number; - layout: Size; - summary: Size; + colwidth: number = LABEL; - constructor(state: StateCallbacks, marker: string) { + constructor(state: StateCallbacks, marker: Ast.marker) { this.marker = marker; this.state = state; - this.layout = EMPTY; - this.summary = EMPTY; this.requestProbeInfo = this.requestProbeInfo.bind(this); this.setPersistent = this.setPersistent.bind(this); this.setTransient = this.setTransient.bind(this); diff --git a/src/plugins/value/api/values_request.ml b/src/plugins/value/api/values_request.ml index 951a32b6d7b..a9a456b460e 100644 --- a/src/plugins/value/api/values_request.ml +++ b/src/plugins/value/api/values_request.ml @@ -185,8 +185,16 @@ end (* --- Domain Utilities --- *) (* -------------------------------------------------------------------------- *) -module Jcallstack = Data.Index(Value_types.Callstack.Map) - (struct let name = "eva-callstack-id" end) +module Jcallstack : S with type t = callstack = +struct + module I = Data.Index + (Value_types.Callstack.Map) + (struct let name = "eva-callstack-id" end) + let jtype = Data.declare ~package ~name:"callstack" I.jtype + type t = I.t + let to_json = I.to_json + let of_json = I.of_json +end module Jcallsite : Data.S with type t = Value_types.call_site = struct @@ -445,8 +453,8 @@ let () = let get_tgt = Request.param getValues ~name:"target" ~descr:(Md.plain "Works with all markers containing an expression") (module Jmarker) in - let get_cs = Request.param_opt getValues ~name:"callstacks" - ~descr:(Md.plain "Callstacks to collect (defaults to all)") + let get_cs = Request.param_opt getValues ~name:"callstack" + ~descr:(Md.plain "Callstack to collect (defaults to none)") (module Jcallstack) in let set_alarms = Request.result getValues ~name:"alarms" ~descr:(Md.plain "Alarms raised during evaluation") -- GitLab