gesso-devtools 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # gesso-devtools
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - **`proofPanel`, the main-thread strip a worker app is judged by.** A pulse on the
8
+ main thread's own animation frame, a button that blocks the thread and records
9
+ when on the render worker's clock, and the render worker's rate, worst gap and
10
+ median frame work — with `layout`, the layout heatmap and re-measure count too.
11
+ The recording is published on a global for a budget script to read, including
12
+ the last block's window. The arithmetic is `ProofRecording`, usable without a
13
+ DOM.
14
+ - Updated dependencies
15
+ - gesso-framework@0.4.1
16
+
17
+ ## 0.4.0
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - Updated dependencies
23
+ - Updated dependencies
24
+ - Updated dependencies
25
+ - Updated dependencies
26
+ - gesso-framework@0.4.0
27
+
3
28
  ## 0.3.0
4
29
 
5
30
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActionCause, ActionEntry, ChannelErrorEntry, ChannelPort, CommandEntry, DevtoolsEvent, DevtoolsRequest, FrameEntry, FrameMetrics, PatchEntry, RuntimeErrorSource, ShellToRuntimeMessage, UiFramePhase, UiNodeReport, UiTreeNode, WorkerHandle } from "gesso-framework";
1
+ import { ActionCause, ActionEntry, ChannelErrorEntry, ChannelPort, CommandEntry, DevtoolsEvent, DevtoolsRequest, FrameEntry, FrameMetrics, PatchEntry, RuntimeErrorSource, ShellToRuntimeMessage, UiFramePhase, UiNodeReport, UiTreeNode, WorkerApp, WorkerHandle } from "gesso-framework";
2
2
  //#region src/sourceMap.d.ts
3
3
  /**
4
4
  * Enough of Source Map v3 to turn a compiled stack frame back into the
@@ -1072,5 +1072,143 @@ declare function shortenPath(url: string, origin?: string): string;
1072
1072
  /** `path:line:column` for a frame, mapped when it could be. */
1073
1073
  declare function formatFrame(frame: StackFrame, origin?: string): string;
1074
1074
  //#endregion
1075
- export { type ActionCause, type ActionEntry, type ActionLog, type ActionLogOptions, type ActionLogPanel, type ActionLogPanelOptions, type ActionLogToken, type ChannelErrorEntry, type CodeFrame, type CodeFrameLine, type CommandEntry, type ConnectDevtoolsOptions, type DevtoolsApp, type DevtoolsAppInfo, type DevtoolsHook, type DevtoolsPanel, type DevtoolsPanelOptions, type DevtoolsPanelTheme, type DevtoolsPicker, type DevtoolsPort, ENVELOPE_SOURCE, type Envelope, type ErrorOrigin, ErrorOverlay, type ErrorOverlayOptions, type FrameEntry, type FrameProfiler, type FrameProfilerOptions, type FrameSample, type FrameSummary, HOOK_PROPERTY, type HookHost, NODE_REPORT_STYLES, type NodeInspector, type NodeInspectorOptions, type NodePickerOptions, type NodeReportViewOptions, type OriginalPosition, type PageMessage, type PagePort, type PanelMessage, type PanelPort, type PatchEntry, type RenderWorkerHost, type RenderWorkerTap, type RenderWorkerTapOptions, SourceMapConsumer, SourceMapStore, type SourceMapV3, type StackFrame, type StackLocation, type TreeRow, type WindowLike, actionGlyph, codeFrame, connectDevtools, createActionLog, createDirectPorts, createNodePicker, decodeMappings, describeActionEntry, formatFrame, forwardNewEntries, getDevtoolsHook, idsToDepth, inputLabel, isEnvelope, mapStack, mountActionLogPanel, mountDevtoolsPanel, mountErrorOverlay, mountFrameProfiler, mountNodeInspector, parseSourceMappingUrl, parseStack, pathTo, primaryFrame, renderNodeReport, rowLabel, shortenPath, summarize, tapRenderWorker, treeRows, windowPagePort, windowPanelPort };
1075
+ //#region src/ProofPanel.d.ts
1076
+ /**
1077
+ * The proof strip: a bar of instruments above a worker application,
1078
+ * and the only part of it that is DOM on the main thread.
1079
+ *
1080
+ * On purpose. The claim a worker application makes is that the main
1081
+ * thread is not doing the work, and numbers the render worker prints on
1082
+ * its own canvas are numbers a stranger has no reason to believe. So the
1083
+ * readout lives where the doubt lives. Block this thread and the strip
1084
+ * stops dead — the pulse freezes, the button stays pressed — while the
1085
+ * two threads behind it carry on, because the application is not here.
1086
+ *
1087
+ * What it has:
1088
+ *
1089
+ * - **a pulse**, driven by this thread's own `requestAnimationFrame`,
1090
+ * which stops the moment the thread is busy;
1091
+ * - **a block button**, which holds this thread in a busy loop for a
1092
+ * few seconds and records when, on the render worker's clock, so
1093
+ * frames drawn *inside* the block can be told from those either side;
1094
+ * - **the render worker's readout**: its frame rate and worst gap over
1095
+ * the last second, and the median work a frame cost;
1096
+ * - **this thread's frame rate**, which drops to nothing in a block;
1097
+ * - with `layout`, **the engine's layout heatmap** and a count of the
1098
+ * nodes each frame re-measured, for an application whose claim is
1099
+ * about measuring less.
1100
+ *
1101
+ * And a recording for a script: `globalThis[options.global]` has the
1102
+ * last two thousand frames, a reset, and the last block's window. A
1103
+ * budget check reads the same numbers the strip is showing, rather than
1104
+ * a measurement it installed for itself.
1105
+ *
1106
+ * Made out of gessosheet's and gessologic's strips, which had been
1107
+ * copied from one to the other and changed a little. The one thing
1108
+ * gessologic's copy found that is worth knowing before writing a check
1109
+ * against this: count frames inside `lastBlock()`, not the whole
1110
+ * recording. A count of the whole recording takes in the frames drawn
1111
+ * just before and after a block, and passes a render worker that drew
1112
+ * nothing at all through it.
1113
+ *
1114
+ * const panel = proofPanel(host, { global: 'myAppProof' });
1115
+ * const app = createApp({ ...panel.options });
1116
+ * panel.attach(app);
1117
+ * app.mount(host);
1118
+ *
1119
+ * Built before `createApp`, because `onFrame` is a constructor option,
1120
+ * and inserted before the host so the canvas measures its final size.
1121
+ */
1122
+ interface ProofPanelOptions {
1123
+ /** The global the recording is published on. Default `'gessoProof'`. */
1124
+ readonly global?: string;
1125
+ /** How long the block button holds the thread, in milliseconds. Default 5,000. */
1126
+ readonly blockMs?: number;
1127
+ /** Add the layout heatmap toggle and the re-measured count. Default false. */
1128
+ readonly layout?: boolean;
1129
+ }
1130
+ /** One frame, as a budget check reads it. */
1131
+ interface ProofFrame {
1132
+ /** When the render worker finished it, on the render worker's clock. */
1133
+ readonly at: number;
1134
+ readonly durationMs: number;
1135
+ readonly measured: number;
1136
+ readonly nodes: number;
1137
+ readonly inputLatencyMs: number | null;
1138
+ /** Which backend drew, and where the frame's time went. */
1139
+ readonly renderer: string;
1140
+ readonly phases: Record<string, number>;
1141
+ readonly gpu: Record<string, number> | null;
1142
+ }
1143
+ /** What a script reads off the global. */
1144
+ interface ProofHandle {
1145
+ frames(): readonly ProofFrame[];
1146
+ reset(): void;
1147
+ /** The last block, on the render worker's clock; null before one. */
1148
+ lastBlock(): ProofBlock | null;
1149
+ }
1150
+ interface ProofBlock {
1151
+ readonly start: number;
1152
+ readonly end: number;
1153
+ }
1154
+ /** The rolling readout: over the last second of frames, or `idle` when there were none lately. */
1155
+ type ProofReadout = {
1156
+ readonly idle: true;
1157
+ } | {
1158
+ readonly idle: false;
1159
+ readonly fps: number;
1160
+ readonly worstGapMs: number;
1161
+ readonly medianWorkMs: number;
1162
+ };
1163
+ /** How many frames the recording keeps for a script to read back. */
1164
+ declare const PROOF_RECORDING = 2000;
1165
+ /**
1166
+ * The strip's arithmetic, without the DOM: frames in, readout and
1167
+ * recording out.
1168
+ *
1169
+ * Every time is the render worker's. A frame is stamped when the worker
1170
+ * finished it, and those stamps survive a blocked main thread, which
1171
+ * receives the whole block's frames in one burst when it comes back; a
1172
+ * readout that timed its own arrivals would report a stall that never
1173
+ * happened, about the wrong thread. This thread's clock is translated
1174
+ * onto the worker's with an offset learned from every frame, because a
1175
+ * worker's `performance.now()` counts from the worker's creation.
1176
+ */
1177
+ declare class ProofRecording implements ProofHandle {
1178
+ private readonly hostNow;
1179
+ private readonly recorded;
1180
+ private readonly finishes;
1181
+ private readonly costs;
1182
+ private offset;
1183
+ private block;
1184
+ private peak;
1185
+ constructor(hostNow?: () => number);
1186
+ /** Takes a frame from the runtime's `onFrame`. */
1187
+ frame(metrics: FrameMetrics): void;
1188
+ /** Now, on the render worker's clock. */
1189
+ workerNow(): number;
1190
+ /** Records a block this thread held from `start` to `end`, on its own clock. */
1191
+ blocked(start: number, end: number): void;
1192
+ readout(): ProofReadout;
1193
+ /** The most nodes any frame since the last reset re-measured. */
1194
+ peakMeasured(): number;
1195
+ frames(): readonly ProofFrame[];
1196
+ lastBlock(): ProofBlock | null;
1197
+ reset(): void;
1198
+ }
1199
+ /**
1200
+ * Puts the strip before `host` and wires it; returns the options the
1201
+ * application has to be created with, and the function that attaches the
1202
+ * controls to it once it exists.
1203
+ */
1204
+ declare function proofPanel(host: HTMLElement, options?: ProofPanelOptions): {
1205
+ readonly recording: ProofRecording;
1206
+ readonly options: {
1207
+ onFrame: (metrics: FrameMetrics) => void;
1208
+ onInspect?: (report: UiNodeReport | null) => void;
1209
+ };
1210
+ readonly attach: (app: WorkerApp) => void;
1211
+ };
1212
+ //#endregion
1213
+ export { type ActionCause, type ActionEntry, type ActionLog, type ActionLogOptions, type ActionLogPanel, type ActionLogPanelOptions, type ActionLogToken, type ChannelErrorEntry, type CodeFrame, type CodeFrameLine, type CommandEntry, type ConnectDevtoolsOptions, type DevtoolsApp, type DevtoolsAppInfo, type DevtoolsHook, type DevtoolsPanel, type DevtoolsPanelOptions, type DevtoolsPanelTheme, type DevtoolsPicker, type DevtoolsPort, ENVELOPE_SOURCE, type Envelope, type ErrorOrigin, ErrorOverlay, type ErrorOverlayOptions, type FrameEntry, type FrameProfiler, type FrameProfilerOptions, type FrameSample, type FrameSummary, HOOK_PROPERTY, type HookHost, NODE_REPORT_STYLES, type NodeInspector, type NodeInspectorOptions, type NodePickerOptions, type NodeReportViewOptions, type OriginalPosition, PROOF_RECORDING, type PageMessage, type PagePort, type PanelMessage, type PanelPort, type PatchEntry, type ProofBlock, type ProofFrame, type ProofHandle, type ProofPanelOptions, type ProofReadout, ProofRecording, type RenderWorkerHost, type RenderWorkerTap, type RenderWorkerTapOptions, SourceMapConsumer, SourceMapStore, type SourceMapV3, type StackFrame, type StackLocation, type TreeRow, type WindowLike, actionGlyph, codeFrame, connectDevtools, createActionLog, createDirectPorts, createNodePicker, decodeMappings, describeActionEntry, formatFrame, forwardNewEntries, getDevtoolsHook, idsToDepth, inputLabel, isEnvelope, mapStack, mountActionLogPanel, mountDevtoolsPanel, mountErrorOverlay, mountFrameProfiler, mountNodeInspector, parseSourceMappingUrl, parseStack, pathTo, primaryFrame, proofPanel, renderNodeReport, rowLabel, shortenPath, summarize, tapRenderWorker, treeRows, windowPagePort, windowPanelPort };
1076
1214
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -488,7 +488,7 @@ var ErrorOverlay = class {
488
488
  this.container.style.display = "none";
489
489
  this.root = this.container.attachShadow({ mode: "open" });
490
490
  const style = this.doc.createElement("style");
491
- style.textContent = STYLES$4;
491
+ style.textContent = STYLES$5;
492
492
  this.panel = this.doc.createElement("div");
493
493
  this.panel.className = "panel";
494
494
  this.panel.tabIndex = -1;
@@ -752,7 +752,7 @@ function button(doc, label, title, onClick) {
752
752
  * rather than a part of the app, and it should never be mistaken for
753
753
  * one.
754
754
  */
755
- const STYLES$4 = `
755
+ const STYLES$5 = `
756
756
  :host { all: initial; }
757
757
  .scrim {
758
758
  position: absolute;
@@ -1170,7 +1170,7 @@ function mountActionLogPanel(host, log, options = {}) {
1170
1170
  const container = doc.createElement("div");
1171
1171
  const root = container.attachShadow({ mode: "open" });
1172
1172
  const style = doc.createElement("style");
1173
- style.textContent = STYLES$3;
1173
+ style.textContent = STYLES$4;
1174
1174
  const panel = doc.createElement("div");
1175
1175
  panel.className = `panel ${options.corner ?? "bottom-right"}`;
1176
1176
  panel.hidden = true;
@@ -1312,7 +1312,7 @@ function formatOffset(ms) {
1312
1312
  if (ms < 1e3) return `${Math.round(ms)}ms`;
1313
1313
  return `${(ms / 1e3).toFixed(1)}s`;
1314
1314
  }
1315
- const STYLES$3 = `
1315
+ const STYLES$4 = `
1316
1316
  :host { all: initial; }
1317
1317
  .panel {
1318
1318
  position: absolute;
@@ -1985,7 +1985,7 @@ function mountFrameProfiler(host, options = {}) {
1985
1985
  const container = doc.createElement("div");
1986
1986
  const root = container.attachShadow({ mode: "open" });
1987
1987
  const style = doc.createElement("style");
1988
- style.textContent = STYLES$2;
1988
+ style.textContent = STYLES$3;
1989
1989
  const panel = doc.createElement("div");
1990
1990
  panel.className = docked ? "panel docked" : `panel ${options.corner ?? "top-right"}`;
1991
1991
  panel.hidden = true;
@@ -2132,7 +2132,7 @@ function stat(doc, label, value) {
2132
2132
  element.append(name, doc.createTextNode(value));
2133
2133
  return element;
2134
2134
  }
2135
- const STYLES$2 = `
2135
+ const STYLES$3 = `
2136
2136
  :host { all: initial; }
2137
2137
  .panel {
2138
2138
  position: absolute;
@@ -2252,7 +2252,7 @@ function mountDevtoolsPanel(host, port, options = {}) {
2252
2252
  applyTheme(options.theme ?? "auto");
2253
2253
  const root = container.attachShadow({ mode: "open" });
2254
2254
  const style = doc.createElement("style");
2255
- style.textContent = STYLES$1;
2255
+ style.textContent = STYLES$2;
2256
2256
  root.append(style);
2257
2257
  host.appendChild(container);
2258
2258
  const toolbar = el(doc, "div", "toolbar");
@@ -2836,7 +2836,7 @@ const DARK_TOKENS = `
2836
2836
  --gd-row-selected: #1f3b5c;
2837
2837
  --gd-budget: rgba(255, 255, 255, 0.35);
2838
2838
  `;
2839
- const STYLES$1 = `
2839
+ const STYLES$2 = `
2840
2840
  :host { all: initial; display: block; height: 100%;
2841
2841
  --gd-bg: #ffffff;
2842
2842
  --gd-bg-raised: #f1f3f4;
@@ -2960,7 +2960,7 @@ function mountNodeInspector(host, options = {}) {
2960
2960
  const container = doc.createElement("div");
2961
2961
  const root = container.attachShadow({ mode: "open" });
2962
2962
  const style = doc.createElement("style");
2963
- style.textContent = STYLES;
2963
+ style.textContent = STYLES$1;
2964
2964
  const panel = doc.createElement("div");
2965
2965
  panel.className = `panel ${options.corner ?? "bottom-left"}`;
2966
2966
  panel.hidden = true;
@@ -2986,7 +2986,7 @@ function mountNodeInspector(host, options = {}) {
2986
2986
  }
2987
2987
  };
2988
2988
  }
2989
- const STYLES = `
2989
+ const STYLES$1 = `
2990
2990
  :host { all: initial; }
2991
2991
  .panel {
2992
2992
  position: absolute;
@@ -3013,6 +3013,259 @@ const STYLES = `
3013
3013
  ${NODE_REPORT_STYLES}
3014
3014
  `;
3015
3015
  //#endregion
3016
- export { ENVELOPE_SOURCE, ErrorOverlay, HOOK_PROPERTY, NODE_REPORT_STYLES, SourceMapConsumer, SourceMapStore, actionGlyph, codeFrame, connectDevtools, createActionLog, createDirectPorts, createNodePicker, decodeMappings, describeActionEntry, formatFrame, forwardNewEntries, getDevtoolsHook, idsToDepth, inputLabel, isEnvelope, mapStack, mountActionLogPanel, mountDevtoolsPanel, mountErrorOverlay, mountFrameProfiler, mountNodeInspector, parseSourceMappingUrl, parseStack, pathTo, primaryFrame, renderNodeReport, rowLabel, shortenPath, summarize, tapRenderWorker, treeRows, windowPagePort, windowPanelPort };
3016
+ //#region src/ProofPanel.ts
3017
+ /** How many frames the recording keeps for a script to read back. */
3018
+ const PROOF_RECORDING = 2e3;
3019
+ /**
3020
+ * How far back the rolling readout looks, in milliseconds.
3021
+ *
3022
+ * A time window and not a count of frames: an idle Gesso application
3023
+ * draws nothing at all, so a window of the last ninety frames held
3024
+ * frames from however long ago the last interaction was, and reported a
3025
+ * scroll running at sixty as three.
3026
+ */
3027
+ const RECENT_MS = 1e3;
3028
+ /** Past this since the last frame, the readout says idle rather than a rate that reads as a stall. */
3029
+ const IDLE_AFTER_MS = 400;
3030
+ /**
3031
+ * The strip's arithmetic, without the DOM: frames in, readout and
3032
+ * recording out.
3033
+ *
3034
+ * Every time is the render worker's. A frame is stamped when the worker
3035
+ * finished it, and those stamps survive a blocked main thread, which
3036
+ * receives the whole block's frames in one burst when it comes back; a
3037
+ * readout that timed its own arrivals would report a stall that never
3038
+ * happened, about the wrong thread. This thread's clock is translated
3039
+ * onto the worker's with an offset learned from every frame, because a
3040
+ * worker's `performance.now()` counts from the worker's creation.
3041
+ */
3042
+ var ProofRecording = class {
3043
+ hostNow;
3044
+ recorded = [];
3045
+ finishes = [];
3046
+ costs = [];
3047
+ offset = null;
3048
+ block = null;
3049
+ peak = 0;
3050
+ constructor(hostNow = () => performance.now()) {
3051
+ this.hostNow = hostNow;
3052
+ }
3053
+ /** Takes a frame from the runtime's `onFrame`. */
3054
+ frame(metrics) {
3055
+ this.offset = this.hostNow() - metrics.at;
3056
+ this.recorded.push({
3057
+ at: metrics.at,
3058
+ durationMs: metrics.durationMs,
3059
+ measured: metrics.measured,
3060
+ nodes: metrics.nodes,
3061
+ inputLatencyMs: metrics.inputLatencyMs,
3062
+ renderer: metrics.renderer,
3063
+ phases: { ...metrics.phases },
3064
+ gpu: metrics.gpu === null ? null : { ...metrics.gpu }
3065
+ });
3066
+ if (this.recorded.length > 2e3) this.recorded.shift();
3067
+ this.finishes.push(metrics.at);
3068
+ this.costs.push(metrics.durationMs);
3069
+ while (this.finishes.length > 1 && metrics.at - this.finishes[0] > RECENT_MS) {
3070
+ this.finishes.shift();
3071
+ this.costs.shift();
3072
+ }
3073
+ this.peak = Math.max(this.peak, metrics.measured);
3074
+ }
3075
+ /** Now, on the render worker's clock. */
3076
+ workerNow() {
3077
+ return this.hostNow() - (this.offset ?? 0);
3078
+ }
3079
+ /** Records a block this thread held from `start` to `end`, on its own clock. */
3080
+ blocked(start, end) {
3081
+ const offset = this.offset ?? 0;
3082
+ this.block = {
3083
+ start: start - offset,
3084
+ end: end - offset
3085
+ };
3086
+ }
3087
+ readout() {
3088
+ const last = this.finishes.at(-1);
3089
+ const first = this.finishes[0];
3090
+ if (last === void 0 || first === void 0 || last <= first || this.workerNow() - last > IDLE_AFTER_MS) return { idle: true };
3091
+ let worst = 0;
3092
+ for (let i = 1; i < this.finishes.length; i++) worst = Math.max(worst, this.finishes[i] - this.finishes[i - 1]);
3093
+ const sorted = [...this.costs].sort((a, b) => a - b);
3094
+ return {
3095
+ idle: false,
3096
+ fps: Math.round((this.finishes.length - 1) / (last - first) * 1e3),
3097
+ worstGapMs: worst,
3098
+ medianWorkMs: sorted[Math.floor(sorted.length / 2)]
3099
+ };
3100
+ }
3101
+ /** The most nodes any frame since the last reset re-measured. */
3102
+ peakMeasured() {
3103
+ return this.peak;
3104
+ }
3105
+ frames() {
3106
+ return this.recorded;
3107
+ }
3108
+ lastBlock() {
3109
+ return this.block;
3110
+ }
3111
+ reset() {
3112
+ this.recorded.length = 0;
3113
+ this.finishes.length = 0;
3114
+ this.costs.length = 0;
3115
+ this.peak = 0;
3116
+ }
3117
+ };
3118
+ /**
3119
+ * Puts the strip before `host` and wires it; returns the options the
3120
+ * application has to be created with, and the function that attaches the
3121
+ * controls to it once it exists.
3122
+ */
3123
+ function proofPanel(host, options = {}) {
3124
+ const blockMs = options.blockMs ?? 5e3;
3125
+ const layout = options.layout === true;
3126
+ const doc = host.ownerDocument;
3127
+ const view = doc.defaultView ?? globalThis;
3128
+ build(host, blockMs, layout);
3129
+ const find = (id) => {
3130
+ const found = doc.getElementById(`gesso-proof-${id}`);
3131
+ if (found === null) throw new Error(`The proof strip has no #gesso-proof-${id} element.`);
3132
+ return found;
3133
+ };
3134
+ const pulse = find("pulse");
3135
+ const block = find("block");
3136
+ const fpsOut = find("fps");
3137
+ const gapOut = find("gap");
3138
+ const workOut = find("work");
3139
+ const mainOut = find("mainfps");
3140
+ const recording = new ProofRecording();
3141
+ globalThis[options.global ?? "gessoProof"] = recording;
3142
+ const onFrame = (metrics) => {
3143
+ recording.frame(metrics);
3144
+ if (layout) {
3145
+ find("measured").textContent = String(metrics.measured);
3146
+ find("peak").textContent = String(recording.peakMeasured());
3147
+ }
3148
+ };
3149
+ let mainFrames = 0;
3150
+ let sampledAt = view.performance.now();
3151
+ const tick = (now) => {
3152
+ mainFrames++;
3153
+ pulse.style.opacity = String(.35 + .65 * Math.abs(Math.sin(now / 350)));
3154
+ if (now - sampledAt >= 500) {
3155
+ mainOut.textContent = String(Math.round(mainFrames / (now - sampledAt) * 1e3));
3156
+ mainFrames = 0;
3157
+ sampledAt = now;
3158
+ const readout = recording.readout();
3159
+ fpsOut.textContent = readout.idle ? "idle" : String(readout.fps);
3160
+ gapOut.textContent = readout.idle ? "—" : `${readout.worstGapMs.toFixed(1)}ms`;
3161
+ workOut.textContent = readout.idle ? "—" : `${readout.medianWorkMs.toFixed(1)}ms`;
3162
+ }
3163
+ view.requestAnimationFrame(tick);
3164
+ };
3165
+ view.requestAnimationFrame(tick);
3166
+ const attach = (app) => {
3167
+ block.addEventListener("click", () => {
3168
+ block.disabled = true;
3169
+ block.textContent = `Blocking for ${blockMs / 1e3}s…`;
3170
+ pulse.classList.add("blocked");
3171
+ view.requestAnimationFrame(() => view.requestAnimationFrame(() => {
3172
+ const start = view.performance.now();
3173
+ const until = start + blockMs;
3174
+ while (view.performance.now() < until);
3175
+ recording.blocked(start, view.performance.now());
3176
+ block.disabled = false;
3177
+ block.textContent = `Block the main thread for ${blockMs / 1e3}s`;
3178
+ pulse.classList.remove("blocked");
3179
+ }));
3180
+ });
3181
+ if (layout) {
3182
+ const heatmap = find("heatmap");
3183
+ const explain = find("explain");
3184
+ heatmap.addEventListener("change", () => {
3185
+ app.setInspector(heatmap.checked);
3186
+ explain.hidden = !heatmap.checked;
3187
+ if (!heatmap.checked) explain.textContent = "";
3188
+ });
3189
+ }
3190
+ };
3191
+ const onInspect = (report) => {
3192
+ const heatmap = find("heatmap");
3193
+ const explain = find("explain");
3194
+ if (!heatmap.checked) return;
3195
+ if (report === null) {
3196
+ explain.textContent = "Point at something.";
3197
+ return;
3198
+ }
3199
+ const { x, y, width, height } = report.box;
3200
+ const label = report.semantics?.label;
3201
+ explain.textContent = `${report.type}${label === void 0 ? "" : ` "${label}"`} — ${width.toFixed(1)}x${height.toFixed(1)} at ${x.toFixed(1)},${y.toFixed(1)}\n\n` + report.explanation;
3202
+ };
3203
+ return {
3204
+ recording,
3205
+ options: layout ? {
3206
+ onFrame,
3207
+ onInspect
3208
+ } : { onFrame },
3209
+ attach
3210
+ };
3211
+ }
3212
+ const STYLES = `
3213
+ body { display: flex; flex-direction: column; }
3214
+ body > .gesso-proof-host { flex: 1; min-height: 0; height: auto !important; }
3215
+ #gesso-proof {
3216
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
3217
+ padding: 6px 10px; background: #22242a; color: #d7d9e0;
3218
+ border-bottom: 1px solid #000; font: 13px system-ui, sans-serif;
3219
+ }
3220
+ #gesso-proof button {
3221
+ font: inherit; padding: 4px 9px; border-radius: 5px;
3222
+ border: 1px solid #4a4d57; background: #32353e; color: #e8eaf0; cursor: pointer;
3223
+ }
3224
+ #gesso-proof button:hover { background: #3d414c; }
3225
+ #gesso-proof label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
3226
+ #gesso-proof .stat { font-variant-numeric: tabular-nums; white-space: nowrap; }
3227
+ #gesso-proof .stat b { color: #fff; font-weight: 600; }
3228
+ #gesso-proof .sep { width: 1px; align-self: stretch; background: #4a4d57; }
3229
+ #gesso-proof-pulse { width: 12px; height: 12px; border-radius: 50%; background: #5ec26a; }
3230
+ #gesso-proof-pulse.blocked { background: #d4564f; }
3231
+ #gesso-proof-explain {
3232
+ position: fixed; right: 12px; bottom: 12px; z-index: 1; margin: 0; padding: 9px 11px;
3233
+ max-width: min(680px, calc(100vw - 24px)); max-height: 42vh; overflow: auto;
3234
+ pointer-events: none; font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
3235
+ white-space: pre-wrap; background: #16171bf2; color: #b9bdc7;
3236
+ border: 1px solid #3a3d46; border-radius: 6px; box-shadow: 0 6px 24px #0006;
3237
+ }
3238
+ #gesso-proof-explain[hidden] { display: none; }
3239
+ `;
3240
+ /** The strip, in the order a person reads it, before the host so the canvas measures its final size. */
3241
+ function build(host, blockMs, layout) {
3242
+ const doc = host.ownerDocument;
3243
+ const styles = doc.createElement("style");
3244
+ styles.textContent = STYLES;
3245
+ doc.head.append(styles);
3246
+ host.classList.add("gesso-proof-host");
3247
+ const strip = doc.createElement("div");
3248
+ strip.id = "gesso-proof";
3249
+ strip.innerHTML = `
3250
+ <span id="gesso-proof-pulse" title="The main thread’s own animation frame"></span>
3251
+ <button id="gesso-proof-block" type="button">Block the main thread for ${blockMs / 1e3}s</button>
3252
+ ${layout ? "<label><input id=\"gesso-proof-heatmap\" type=\"checkbox\" />Layout heatmap</label>" : ""}
3253
+ <span class="sep"></span>
3254
+ <span class="stat">render worker <b id="gesso-proof-fps">—</b> fps</span>
3255
+ <span class="stat">worst frame gap <b id="gesso-proof-gap">—</b></span>
3256
+ <span class="stat">frame work <b id="gesso-proof-work">—</b> median</span>
3257
+ ${layout ? "<span class=\"stat\">re-measured <b id=\"gesso-proof-measured\">—</b> last frame, at most <b id=\"gesso-proof-peak\">—</b></span>" : ""}
3258
+ <span class="stat">main thread <b id="gesso-proof-mainfps">—</b> fps</span>
3259
+ `;
3260
+ host.before(strip);
3261
+ if (layout) {
3262
+ const explain = doc.createElement("pre");
3263
+ explain.id = "gesso-proof-explain";
3264
+ explain.hidden = true;
3265
+ host.before(explain);
3266
+ }
3267
+ }
3268
+ //#endregion
3269
+ export { ENVELOPE_SOURCE, ErrorOverlay, HOOK_PROPERTY, NODE_REPORT_STYLES, PROOF_RECORDING, ProofRecording, SourceMapConsumer, SourceMapStore, actionGlyph, codeFrame, connectDevtools, createActionLog, createDirectPorts, createNodePicker, decodeMappings, describeActionEntry, formatFrame, forwardNewEntries, getDevtoolsHook, idsToDepth, inputLabel, isEnvelope, mapStack, mountActionLogPanel, mountDevtoolsPanel, mountErrorOverlay, mountFrameProfiler, mountNodeInspector, parseSourceMappingUrl, parseStack, pathTo, primaryFrame, proofPanel, renderNodeReport, rowLabel, shortenPath, summarize, tapRenderWorker, treeRows, windowPagePort, windowPanelPort };
3017
3270
 
3018
3271
  //# sourceMappingURL=index.js.map