From 5897542b0f5334b10e5a51fda10596465361477b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr> Date: Thu, 17 Dec 2020 14:34:36 +0100 Subject: [PATCH] [ivette/eva] stack layout --- ivette/src/dome/src/renderer/dome.tsx | 9 ++- ivette/src/frama-c/eva/Values.tsx | 54 ++++++++++-------- ivette/src/frama-c/eva/layout.ts | 81 ++++++++++++++++++++------- ivette/src/frama-c/eva/model.ts | 80 ++++++++++++++------------ ivette/src/frama-c/eva/style.css | 11 ++++ 5 files changed, 151 insertions(+), 84 deletions(-) diff --git a/ivette/src/dome/src/renderer/dome.tsx b/ivette/src/dome/src/renderer/dome.tsx index 237420fec39..12606069a35 100644 --- a/ivette/src/dome/src/renderer/dome.tsx +++ b/ivette/src/dome/src/renderer/dome.tsx @@ -490,11 +490,10 @@ export function useForceUpdate() { export function useUpdate(...events: Event<any>[]) { const fn = useForceUpdate(); React.useEffect(() => { - if (events.length === 0) events.push(update); - events.forEach((evt) => evt.on(fn)); - return () => events.forEach((evt) => evt.off(fn)); - }, [fn, ...events]); // eslint-disable-line react-hooks/exhaustive-deps - // The rule signals events is missing, probably because of « … » + const theEvents = events ? events.slice() : [update]; + theEvents.forEach((evt) => evt.on(fn)); + return () => theEvents.forEach((evt) => evt.off(fn)); + }); } // -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/eva/Values.tsx b/ivette/src/frama-c/eva/Values.tsx index 986e41886b3..120b58b4db5 100644 --- a/ivette/src/frama-c/eva/Values.tsx +++ b/ivette/src/frama-c/eva/Values.tsx @@ -24,7 +24,7 @@ import * as Ast from 'frama-c/api/kernel/ast'; // Locals import { SizedArea, HSIZER, WSIZER } from './sized'; import { sizeof } from './cells'; -import { RowKind } from './layout'; +import { Row } from './layout'; import { Probe } from './probes'; import { Model, getModelInstance } from './model'; import './style.css'; @@ -35,7 +35,7 @@ import './style.css'; function useModel(): Model { const model = getModelInstance(); - Dome.useUpdate(model.signal); + Dome.useUpdate(model.changed, model.laidout); return model; } @@ -90,8 +90,8 @@ function ProbeEditor() { // -------------------------------------------------------------------------- interface TableCellProps { - kind: RowKind; probe: Probe; + row: Row; } const CELLPADDING = 4; @@ -99,32 +99,36 @@ const CELLPADDING = 4; function TableCell(props: TableCellProps) { const model = useModel(); const [selection, setSelection] = States.useSelection(); - const { probe, kind } = props; + const { probe, row } = props; + const { kind, callstack } = row; const minWidth = CELLPADDING + WSIZER.dimension(probe.minCols); const maxWidth = CELLPADDING + WSIZER.dimension(probe.maxCols); const style = { width: minWidth, maxWidth }; - let styling = 'dome-text-code'; let contents: React.ReactNode = props.probe.marker; const { transient } = probe; + switch (kind) { + + // ---- Probe Contents case 'probes': if (transient) { - styling = 'dome-text-label'; - contents = '« Probe »'; + contents = <span className="dome-text-label">« Probe »</span>; } else { const { rank, code, label } = probe; const atpoint = rank && ( - <span className='dome-text-code eva-probe-stmt'>@S{probe.rank}</span> + <span className="eva-probe-stmt">@S{rank}</span> ); - styling = 'dome-text-label'; contents = ( - <>{label ?? code}{atpoint}</> + <span className="dome-text-label">{label ?? code}{atpoint}</span> ); } break; + + // ---- Values Contents case 'values': + case 'callstack': { - const { values } = model.values.getValues(probe.marker); + const { values } = model.values.getValues(probe.marker, callstack); const { cols, rows } = sizeof(values); contents = ( <SizedArea cols={cols} rows={rows}> @@ -133,11 +137,13 @@ function TableCell(props: TableCellProps) { ); } break; + } + + // --- Cell Packing const isFocused = model.getFocused() === probe; const className = classes( 'eva-cell', - styling, transient && 'eva-transient', !transient && isFocused && 'eva-focused', ); @@ -174,16 +180,23 @@ function TableRow(props: TableRowProps) { if (!row) return null; const { kind, probes } = row; const className = `eva-${kind}`; + const sk = row.stackIndex; + const header = row.stacks && ( + <div className="eva-cell eva-stack"> + {sk === undefined ? '#' : `${1 + sk}`} + </div> + ); const contents = probes.map((probe) => ( <TableCell key={probe.marker} - kind={kind} probe={probe} + row={row} /> )); return ( <Hpack className={className} style={props.style}> <div className="eva-row"> + {header} {contents} </div> <Filler /> @@ -205,16 +218,13 @@ function ValuesPanel(props: Dimension) { const { width, height } = props; // --- reset line cache const listRef = React.useRef<VariableSizeList>(null); - const forceGridLayout = React.useCallback( - () => { - const vlist = listRef.current; - if (vlist) vlist.resetAfterIndex(0, true); - }, - [listRef], - ); + Dome.useEvent(model.laidout, () => { + const vlist = listRef.current; + if (vlist) vlist.resetAfterIndex(0, true); + }); // --- compute line height const getRowHeight = React.useCallback( - (k: number) => HSIZER.dimension(model.getRowHeight(k)), + (k: number) => HSIZER.dimension(model.getRowLines(k)), [model], ); // --- compute layout @@ -223,7 +233,7 @@ function ValuesPanel(props: Dimension) { const [selection] = States.useSelection(); React.useEffect(() => { const target = Ast.jMarker(selection?.current?.marker); - model.setLayout({ margin, target }, forceGridLayout); + model.setLayout({ margin, target }); }); // --- render list return ( diff --git a/ivette/src/frama-c/eva/layout.ts b/ivette/src/frama-c/eva/layout.ts index e3a95b2dcb3..f060cf90cd6 100644 --- a/ivette/src/frama-c/eva/layout.ts +++ b/ivette/src/frama-c/eva/layout.ts @@ -2,8 +2,10 @@ /* --- Layout ---*/ /* --------------------------------------------------------------------------*/ -import { Size, EMPTY, addH, ValueCache } from './cells'; +import { callstack } from 'frama-c/api/plugins/eva/values'; import { Probe } from './probes'; +import { StacksCache } from './stacks'; +import { Size, EMPTY, addH, ValueCache } from './cells'; export interface LayoutProps { zoom?: number; @@ -16,7 +18,11 @@ export interface Row { key: string; kind: RowKind; probes: Probe[]; - height: number; + headstack?: string; + stacks?: number; + stackIndex?: number; + callstack?: callstack; + hlines: number; } /* --------------------------------------------------------------------------*/ @@ -31,16 +37,19 @@ export class LayoutEngine { // --- Setup - private readonly cache: ValueCache; + private readonly values: ValueCache; + private readonly stacks: StacksCache; private readonly hcrop: number; private readonly vcrop: number; private readonly margin: number; constructor( - cache: ValueCache, props: undefined | LayoutProps, + values: ValueCache, + stacks: StacksCache, ) { - this.cache = cache; + this.values = values; + this.stacks = stacks; const zoom = Math.max(0, props?.zoom ?? 0); this.vcrop = VCROP + 2 * zoom; this.hcrop = HCROP + zoom; @@ -49,6 +58,7 @@ export class LayoutEngine { } // --- Probe Buffer + private byStacks?: string; // stmt private rowSize: Size = EMPTY; private buffer: Probe[] = []; private rows: Row[] = []; @@ -61,33 +71,64 @@ export class LayoutEngine { } push(p: Probe) { - const probeSize = this.cache.getProbeSize(p.marker); + const probeSize = this.values.getProbeSize(p.marker); const s = this.crop(probeSize); p.minCols = s.cols; p.maxCols = Math.max(p.minCols, probeSize.cols); - if (s.cols + this.rowSize.cols > this.margin) this.flush(); + const stmt = p.byCallstacks ? p.stmt : undefined; + if (stmt !== this.byStacks) { + this.flush(); + this.byStacks = stmt; + } + if (!stmt && s.cols + this.rowSize.cols > this.margin) + this.flush(); this.rowSize = addH(this.rowSize, s); this.rowSize.cols += PADDING; this.buffer.push(p); } - // --- Flush Buffer + // --- Flush Rows + flush(): Row[] { const ps = this.buffer; const rs = this.rows; if (ps.length > 0) { - const n = rs.length; - rs.push({ - key: `P${n}`, - kind: 'probes', - probes: ps, - height: 1, - }, { - key: `V${n}`, - kind: 'values', - probes: ps, - height: this.rowSize.rows, - }); + const stmt = this.byStacks; + if (stmt) { + // --- by callstacks + const wcs = this.stacks.getStacks(stmt); + rs.push({ + key: `P${stmt}`, + kind: 'probes', + probes: ps, + stacks: wcs.length, + hlines: 1, + }); + wcs.forEach((cs, k) => { + rs.push({ + key: `C${cs}`, + kind: 'callstack', + probes: ps, + stackIndex: k, + stacks: wcs.length, + hlines: this.values.getStackSize(cs).rows, + }); + }); + } else { + // --- by callstacks + const n = rs.length; + rs.push({ + key: `P${n}`, + kind: 'probes', + probes: ps, + hlines: 1, + }, { + key: `V${n}`, + kind: 'values', + probes: ps, + hlines: this.rowSize.rows, + }); + } } this.buffer = []; this.rowSize = EMPTY; diff --git a/ivette/src/frama-c/eva/model.ts b/ivette/src/frama-c/eva/model.ts index 7f749f28ed4..ef22d1d2eba 100644 --- a/ivette/src/frama-c/eva/model.ts +++ b/ivette/src/frama-c/eva/model.ts @@ -14,7 +14,7 @@ import * as Ast from 'frama-c/api/kernel/ast'; // Model import { Probe } from './probes'; import { StacksCache } from './stacks'; -import { callback, StateCallbacks, ValueCache } from './cells'; +import { StateCallbacks, ValueCache } from './cells'; import { LayoutProps, LayoutEngine, Row } from './layout'; export interface ModelLayout extends LayoutProps { @@ -35,7 +35,7 @@ export class Model implements StateCallbacks { this.setLayout = throttle(this.setLayout.bind(this), 300); this.getRowKey = this.getRowKey.bind(this); this.getRowCount = this.getRowCount.bind(this); - this.getRowHeight = this.getRowHeight.bind(this); + this.getRowLines = this.getRowLines.bind(this); Server.onSignal(Values.changed, this.forceReload); } @@ -76,6 +76,36 @@ export class Model implements StateCallbacks { private layout: ModelLayout = { margin: 80 }; private rows: Row[] = []; + getRow(index: number): Row | undefined { + return this.rows[index]; + } + + getRowCount() { + return this.rows.length; + } + + getRowKey(index: number): string { + const row = this.rows[index]; + return row ? row.key : `#${index}`; + } + + getRowLines(index: number): number { + const row = this.rows[index]; + return row ? row.hlines : 0; + } + + // --- Throttled + setLayout(ly: ModelLayout) { + if (!equal(this.layout, ly)) { + this.layout = ly; + const target = Ast.jMarker(ly.target); + this.selected = target && this.getProbe(target); + this.forceUpdate(); + } + } + + // --- Recompute Layout + private computeLayout() { this.forcedLayout = false; const s = this.selected; @@ -98,39 +128,14 @@ export class Model implements StateCallbacks { toLayout.push(p); } }); - const engine = new LayoutEngine(this.values, this.layout); + const engine = new LayoutEngine( + this.layout, + this.values, + this.stacks, + ); toLayout.sort(Probe.order).forEach(engine.push); this.rows = engine.flush(); - this.forceUpdate(); - } - - getRow(index: number): Row | undefined { - return this.rows[index]; - } - - getRowCount() { - return this.rows.length; - } - - getRowKey(index: number): string { - const row = this.rows[index]; - return row ? row.key : `#${index}`; - } - - getRowHeight(index: number): number { - const row = this.rows[index]; - return row ? row.height : 0; - } - - // --- Throttled - setLayout(ly: ModelLayout, forceGridLayout: callback) { - if (!equal(this.layout, ly)) { - this.layout = ly; - const target = Ast.jMarker(ly.target); - this.selected = target && this.getProbe(target); - this.forceLayout(); - forceGridLayout(); - } + this.laidout.emit(); } // --- Force Reload (empty caches) @@ -144,6 +149,10 @@ export class Model implements StateCallbacks { this.forceLayout(); } + // --- Events + readonly changed = new Dome.Event('eva-changed'); + readonly laidout = new Dome.Event('eva-laidout'); + // --- Force Layout forceLayout() { if (!this.forcedLayout) { @@ -153,10 +162,7 @@ export class Model implements StateCallbacks { } // --- Foce Update - readonly signal = new Dome.Event('eva-force-update'); - forceUpdate() { - this.signal.emit(); - } + forceUpdate() { this.changed.emit(); } } diff --git a/ivette/src/frama-c/eva/style.css b/ivette/src/frama-c/eva/style.css index 7afd3a1d6c7..dea6379e551 100644 --- a/ivette/src/frama-c/eva/style.css +++ b/ivette/src/frama-c/eva/style.css @@ -71,6 +71,13 @@ /* --- Table Cells --- */ /* -------------------------------------------------------------------------- */ +.eva-stack { + width: 12px; + padding-top: 1px; + color: #777; + text-align: center; +} + .eva-cell { flex: 1 1 auto; border-left: thin solid black; @@ -121,4 +128,8 @@ background: #def6ff; } +.eva-cell.eva-stack { + background: #eee; +} + /* -------------------------------------------------------------------------- */ -- GitLab