@buoy-gg/benchmark 2.1.3 → 2.1.4-beta.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.
Files changed (34) hide show
  1. package/lib/commonjs/benchmarking/BenchmarkComparator.js +221 -1
  2. package/lib/commonjs/benchmarking/BenchmarkRecorder.js +497 -1
  3. package/lib/commonjs/benchmarking/BenchmarkStorage.js +235 -1
  4. package/lib/commonjs/benchmarking/index.js +83 -1
  5. package/lib/commonjs/benchmarking/types.js +13 -1
  6. package/lib/commonjs/components/BenchmarkCompareView.js +475 -1
  7. package/lib/commonjs/components/BenchmarkDetailView.js +346 -1
  8. package/lib/commonjs/components/BenchmarkModal.js +505 -1
  9. package/lib/commonjs/components/BenchmarkSessionCard.js +193 -1
  10. package/lib/commonjs/index.js +62 -1
  11. package/lib/commonjs/preset.js +86 -1
  12. package/lib/module/benchmarking/BenchmarkComparator.js +216 -1
  13. package/lib/module/benchmarking/BenchmarkRecorder.js +493 -1
  14. package/lib/module/benchmarking/BenchmarkStorage.js +227 -1
  15. package/lib/module/benchmarking/index.js +48 -1
  16. package/lib/module/benchmarking/types.js +13 -1
  17. package/lib/module/components/BenchmarkCompareView.js +469 -1
  18. package/lib/module/components/BenchmarkDetailView.js +340 -1
  19. package/lib/module/components/BenchmarkModal.js +499 -1
  20. package/lib/module/components/BenchmarkSessionCard.js +187 -1
  21. package/lib/module/index.js +39 -1
  22. package/lib/module/preset.js +81 -1
  23. package/lib/typescript/benchmarking/BenchmarkComparator.d.ts.map +1 -0
  24. package/lib/typescript/benchmarking/BenchmarkRecorder.d.ts.map +1 -0
  25. package/lib/typescript/benchmarking/BenchmarkStorage.d.ts.map +1 -0
  26. package/lib/typescript/benchmarking/index.d.ts.map +1 -0
  27. package/lib/typescript/benchmarking/types.d.ts.map +1 -0
  28. package/lib/typescript/components/BenchmarkCompareView.d.ts.map +1 -0
  29. package/lib/typescript/components/BenchmarkDetailView.d.ts.map +1 -0
  30. package/lib/typescript/components/BenchmarkModal.d.ts.map +1 -0
  31. package/lib/typescript/components/BenchmarkSessionCard.d.ts.map +1 -0
  32. package/lib/typescript/index.d.ts.map +1 -0
  33. package/lib/typescript/preset.d.ts.map +1 -0
  34. package/package.json +2 -2
@@ -1 +1,497 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=exports.benchmarkRecorder=exports.BenchmarkRecorder=void 0;var _reactNative=require("react-native");function generateSessionId(e){return`${e}_${Date.now()}_${Math.random().toString(36).substring(2,8)}`}function percentile(e,t){if(0===e.length)return 0;const s=Math.ceil(t/100*e.length)-1;return e[Math.max(0,Math.min(s,e.length-1))]}function captureMemorySnapshot(){return null}function getBenchmarkContext(e,t){let s="unknown";return"ios"===_reactNative.Platform.OS?s="ios":"android"===_reactNative.Platform.OS?s="android":"web"===_reactNative.Platform.OS&&(s="web"),{platform:s,osVersion:_reactNative.Platform.Version?.toString(),isDev:__DEV__??!1,batchSize:e,showRenderCount:t}}function computeStats(e,t){const s=e.length;if(0===s)return{batchCount:0,totalNodesReceived:0,totalNodesFiltered:0,totalNodesProcessed:0,avgFilterTime:0,avgMeasureTime:0,avgTrackTime:0,avgCallbackTime:0,avgTotalTime:0,minTotalTime:0,maxTotalTime:0,p50TotalTime:0,p95TotalTime:0,p99TotalTime:0,avgOverlayRenderTime:0,avgHighlightsPerRender:0};let o=0,r=0,a=0,i=0,n=0,l=0,c=0,m=0;const d=[];for(const t of e)o+=t.nodesReceived,r+=t.nodesFiltered,a+=t.nodesInBatch,i+=t.filteringTime,n+=t.measurementTime,l+=t.trackingTime,c+=t.callbackTime,m+=t.totalTime,d.push(t.totalTime);d.sort((e,t)=>e-t);let h=0,g=0;for(const e of t)h+=e.renderTime,g+=e.highlightCount;return{batchCount:s,totalNodesReceived:o,totalNodesFiltered:r,totalNodesProcessed:a,avgFilterTime:i/s,avgMeasureTime:n/s,avgTrackTime:l/s,avgCallbackTime:c/s,avgTotalTime:m/s,minTotalTime:d[0],maxTotalTime:d[d.length-1],p50TotalTime:percentile(d,50),p95TotalTime:percentile(d,95),p99TotalTime:percentile(d,99),avgOverlayRenderTime:t.length>0?h/t.length:0,avgHighlightsPerRender:t.length>0?g/t.length:0}}class BenchmarkRecorder{state="idle";sessionId="";sessionName="";sessionStartTime=0;verbose=!1;captureMemory=!0;batches=[];overlayRenders=[];memoryStart=null;memoryEnd=null;marks=[];measures=[];batchSize=150;showRenderCount=!0;listeners=new Set;activeMeasures=new Map;getState(){return this.state}isRecording(){return"recording"===this.state}setBatchSize(e){this.batchSize=e}setShowRenderCount(e){this.showRenderCount=e}startSession(e){"recording"!==this.state?(this.sessionId=generateSessionId(e.name),this.sessionName=e.name,this.sessionDescription=e.description,this.verbose=e.verbose??!1,this.captureMemory=e.captureMemory??!0,this.batches=[],this.overlayRenders=[],this.marks=[],this.measures=[],this.activeMeasures.clear(),this.captureMemory&&(this.memoryStart=captureMemorySnapshot()),this.sessionStartTime=performance.now(),this.marks.push({name:"session_start",startTime:0,detail:{name:this.sessionName}}),this.state="recording",this.verbose&&(console.log(`[BenchmarkRecorder] Session started: ${this.sessionName}`),console.log(` ID: ${this.sessionId}`)),this.notifyListeners("start")):console.warn("[BenchmarkRecorder] Session already recording. Stop it first.")}stopSession(){if("recording"!==this.state)return console.warn("[BenchmarkRecorder] No active session to stop."),null;const e=performance.now()-this.sessionStartTime;this.marks.push({name:"session_end",startTime:e}),this.measures.push({name:"session_total",startTime:0,duration:e}),this.captureMemory&&(this.memoryEnd=captureMemorySnapshot());let t=null;null!=this.memoryStart?.usedJSHeapSize&&null!=this.memoryEnd?.usedJSHeapSize&&(t=this.memoryEnd.usedJSHeapSize-this.memoryStart.usedJSHeapSize);const s=computeStats(this.batches,this.overlayRenders),o={version:"1.0",id:this.sessionId,name:this.sessionName,description:this.sessionDescription,createdAt:Date.now(),duration:e,context:getBenchmarkContext(this.batchSize,this.showRenderCount),batches:[...this.batches],overlayRenders:[...this.overlayRenders],marks:[...this.marks],measures:[...this.measures],stats:s,memoryStart:this.memoryStart,memoryEnd:this.memoryEnd,memoryDelta:t};return this.state="stopped",this.verbose&&this.logReport(o),this.notifyListeners("stop"),o}recordBatch(e){"recording"===this.state&&(this.batches.push(e),this.marks.push({name:`batch_${e.batchId}`,startTime:performance.now()-this.sessionStartTime,detail:{nodesReceived:e.nodesReceived,nodesProcessed:e.nodesInBatch,totalTime:e.totalTime}}),this.verbose&&console.log(`[BenchmarkRecorder] Batch ${e.batchId}: ${e.nodesInBatch} nodes in ${e.totalTime.toFixed(1)}ms`),this.notifyListeners("batch",e))}recordOverlayRender(e,t){if("recording"!==this.state)return;const s={highlightCount:e,renderTime:t,timestamp:performance.now()};this.overlayRenders.push(s),this.marks.push({name:"overlay_render",startTime:performance.now()-this.sessionStartTime,detail:{highlightCount:e,renderTime:t}}),this.verbose&&console.log(`[BenchmarkRecorder] Overlay render: ${e} highlights in ${t.toFixed(1)}ms`),this.notifyListeners("overlay",s)}mark(e,t){"recording"===this.state&&(this.marks.push({name:e,startTime:performance.now()-this.sessionStartTime,detail:t}),this.verbose&&console.log(`[BenchmarkRecorder] Mark: ${e}`))}startMeasure(e){"recording"===this.state&&(this.activeMeasures.set(e,performance.now()),this.verbose&&console.log(`[BenchmarkRecorder] Measure started: ${e}`))}endMeasure(e,t){if("recording"!==this.state)return null;const s=this.activeMeasures.get(e);if(void 0===s)return console.warn(`[BenchmarkRecorder] No active measure: ${e}`),null;const o=performance.now()-s;return this.measures.push({name:e,startTime:s-this.sessionStartTime,duration:o,detail:t}),this.activeMeasures.delete(e),this.verbose&&console.log(`[BenchmarkRecorder] Measure ended: ${e} = ${o.toFixed(1)}ms`),o}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(e,t){for(const s of this.listeners)try{s(e,t)}catch(e){console.error("[BenchmarkRecorder] Error in event listener:",e)}}logReport(e){const{stats:t,memoryDelta:s}=e;if(console.log("\n╔══════════════════════════════════════════════════════════════╗"),console.log("║ BENCHMARK REPORT ║"),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log(`║ Name: ${e.name.padEnd(55)}║`),console.log(`║ ID: ${e.id.substring(0,57).padEnd(57)}║`),console.log(`║ Duration: ${e.duration.toFixed(1).padStart(8)}ms ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ BATCH STATS ║"),console.log(`║ Count: ${t.batchCount.toString().padStart(6)} ║`),console.log(`║ Nodes received: ${t.totalNodesReceived.toString().padStart(8)} ║`),console.log(`║ Nodes processed: ${t.totalNodesProcessed.toString().padStart(7)} ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ TIMING (avg per batch) ║"),console.log(`║ Filter: ${t.avgFilterTime.toFixed(1).padStart(8)}ms ║`),console.log(`║ Measure: ${t.avgMeasureTime.toFixed(1).padStart(7)}ms ← Primary bottleneck ║`),console.log(`║ Track: ${t.avgTrackTime.toFixed(1).padStart(9)}ms ║`),console.log(`║ Callback: ${t.avgCallbackTime.toFixed(1).padStart(6)}ms ║`),console.log(`║ Total: ${t.avgTotalTime.toFixed(1).padStart(9)}ms ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ PERCENTILES ║"),console.log(`║ P50: ${t.p50TotalTime.toFixed(1).padStart(8)}ms ║`),console.log(`║ P95: ${t.p95TotalTime.toFixed(1).padStart(8)}ms ║`),console.log(`║ P99: ${t.p99TotalTime.toFixed(1).padStart(8)}ms ║`),console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ OVERLAY RENDERS ║"),console.log(`║ Avg time: ${t.avgOverlayRenderTime.toFixed(1).padStart(7)}ms ║`),console.log(`║ Avg highlights: ${t.avgHighlightsPerRender.toFixed(0).padStart(5)} ║`),null!=s){const e=(s/1024/1024).toFixed(2),t=s>=0?"+":"";console.log("╠══════════════════════════════════════════════════════════════╣"),console.log("║ MEMORY ║"),console.log(`║ Delta: ${t}${e.padStart(7)}MB ║`)}console.log("╚══════════════════════════════════════════════════════════════╝\n")}}exports.BenchmarkRecorder=BenchmarkRecorder;const benchmarkRecorder=exports.benchmarkRecorder=new BenchmarkRecorder;var _default=exports.default=BenchmarkRecorder;
1
+ /**
2
+ * BenchmarkRecorder
3
+ *
4
+ * Records performance metrics during a benchmark session. Uses performance.now()
5
+ * for high-resolution timing (the only Performance API available in React Native).
6
+ * Collects batch metrics, overlay render times, memory snapshots, and custom marks/measures.
7
+ *
8
+ * Usage:
9
+ * const recorder = new BenchmarkRecorder();
10
+ * recorder.startSession({ name: 'MyBenchmark' });
11
+ *
12
+ * // Record batch metrics (called by HighlightUpdatesController)
13
+ * recorder.recordBatch(batchMetrics);
14
+ *
15
+ * // Record overlay renders (called by HighlightUpdatesOverlay)
16
+ * recorder.recordOverlayRender(count, timeMs);
17
+ *
18
+ * // Add custom marks/measures
19
+ * recorder.mark('customEvent');
20
+ * recorder.startMeasure('apiCall');
21
+ * recorder.endMeasure('apiCall');
22
+ *
23
+ * // Stop and get report
24
+ * const report = recorder.stopSession();
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+
29
+ "use strict";
30
+
31
+ Object.defineProperty(exports, "__esModule", {
32
+ value: true
33
+ });
34
+ exports.default = exports.benchmarkRecorder = exports.BenchmarkRecorder = void 0;
35
+ var _reactNative = require("react-native");
36
+ // React Native only provides performance.now()
37
+
38
+ /**
39
+ * Generate a unique session ID
40
+ */
41
+ function generateSessionId(name) {
42
+ const timestamp = Date.now();
43
+ const random = Math.random().toString(36).substring(2, 8);
44
+ return `${name}_${timestamp}_${random}`;
45
+ }
46
+
47
+ /**
48
+ * Calculate percentile from sorted array
49
+ */
50
+ function percentile(sortedArr, p) {
51
+ if (sortedArr.length === 0) return 0;
52
+ const index = Math.ceil(p / 100 * sortedArr.length) - 1;
53
+ return sortedArr[Math.max(0, Math.min(index, sortedArr.length - 1))];
54
+ }
55
+
56
+ /**
57
+ * Capture current memory snapshot (may not be available in all environments)
58
+ */
59
+ function captureMemorySnapshot() {
60
+ // Memory API is not available in React Native
61
+ // Could potentially use native modules in the future
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Get current benchmark context
67
+ */
68
+ function getBenchmarkContext(batchSize, showRenderCount) {
69
+ let platform = "unknown";
70
+ if (_reactNative.Platform.OS === "ios") platform = "ios";else if (_reactNative.Platform.OS === "android") platform = "android";else if (_reactNative.Platform.OS === "web") platform = "web";
71
+ return {
72
+ platform,
73
+ osVersion: _reactNative.Platform.Version?.toString(),
74
+ isDev: __DEV__ ?? false,
75
+ batchSize,
76
+ showRenderCount
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Compute aggregated statistics from batch and overlay metrics
82
+ */
83
+ function computeStats(batches, overlayRenders) {
84
+ const batchCount = batches.length;
85
+ if (batchCount === 0) {
86
+ return {
87
+ batchCount: 0,
88
+ totalNodesReceived: 0,
89
+ totalNodesFiltered: 0,
90
+ totalNodesProcessed: 0,
91
+ avgFilterTime: 0,
92
+ avgMeasureTime: 0,
93
+ avgTrackTime: 0,
94
+ avgCallbackTime: 0,
95
+ avgTotalTime: 0,
96
+ minTotalTime: 0,
97
+ maxTotalTime: 0,
98
+ p50TotalTime: 0,
99
+ p95TotalTime: 0,
100
+ p99TotalTime: 0,
101
+ avgOverlayRenderTime: 0,
102
+ avgHighlightsPerRender: 0
103
+ };
104
+ }
105
+
106
+ // Sum up totals
107
+ let totalNodesReceived = 0;
108
+ let totalNodesFiltered = 0;
109
+ let totalNodesProcessed = 0;
110
+ let totalFilterTime = 0;
111
+ let totalMeasureTime = 0;
112
+ let totalTrackTime = 0;
113
+ let totalCallbackTime = 0;
114
+ let totalPipelineTime = 0;
115
+ const totalTimes = [];
116
+ for (const batch of batches) {
117
+ totalNodesReceived += batch.nodesReceived;
118
+ totalNodesFiltered += batch.nodesFiltered;
119
+ totalNodesProcessed += batch.nodesInBatch;
120
+ totalFilterTime += batch.filteringTime;
121
+ totalMeasureTime += batch.measurementTime;
122
+ totalTrackTime += batch.trackingTime;
123
+ totalCallbackTime += batch.callbackTime;
124
+ totalPipelineTime += batch.totalTime;
125
+ totalTimes.push(batch.totalTime);
126
+ }
127
+
128
+ // Sort for percentiles
129
+ totalTimes.sort((a, b) => a - b);
130
+
131
+ // Overlay stats
132
+ let totalOverlayTime = 0;
133
+ let totalHighlights = 0;
134
+ for (const render of overlayRenders) {
135
+ totalOverlayTime += render.renderTime;
136
+ totalHighlights += render.highlightCount;
137
+ }
138
+ return {
139
+ batchCount,
140
+ totalNodesReceived,
141
+ totalNodesFiltered,
142
+ totalNodesProcessed,
143
+ avgFilterTime: totalFilterTime / batchCount,
144
+ avgMeasureTime: totalMeasureTime / batchCount,
145
+ avgTrackTime: totalTrackTime / batchCount,
146
+ avgCallbackTime: totalCallbackTime / batchCount,
147
+ avgTotalTime: totalPipelineTime / batchCount,
148
+ minTotalTime: totalTimes[0],
149
+ maxTotalTime: totalTimes[totalTimes.length - 1],
150
+ p50TotalTime: percentile(totalTimes, 50),
151
+ p95TotalTime: percentile(totalTimes, 95),
152
+ p99TotalTime: percentile(totalTimes, 99),
153
+ avgOverlayRenderTime: overlayRenders.length > 0 ? totalOverlayTime / overlayRenders.length : 0,
154
+ avgHighlightsPerRender: overlayRenders.length > 0 ? totalHighlights / overlayRenders.length : 0
155
+ };
156
+ }
157
+
158
+ /**
159
+ * BenchmarkRecorder - Records performance metrics during a benchmark session
160
+ */
161
+ class BenchmarkRecorder {
162
+ state = "idle";
163
+ sessionId = "";
164
+ sessionName = "";
165
+ sessionStartTime = 0;
166
+ verbose = false;
167
+ captureMemory = true;
168
+
169
+ // Collected data
170
+ batches = [];
171
+ overlayRenders = [];
172
+ memoryStart = null;
173
+ memoryEnd = null;
174
+
175
+ // Internal marks and measures (since React Native doesn't have full Performance API)
176
+ marks = [];
177
+ measures = [];
178
+
179
+ // Context
180
+ batchSize = 150;
181
+ showRenderCount = true;
182
+
183
+ // Event listeners
184
+ listeners = new Set();
185
+
186
+ // Active measures (for startMeasure/endMeasure)
187
+ activeMeasures = new Map();
188
+
189
+ /**
190
+ * Get current session state
191
+ */
192
+ getState() {
193
+ return this.state;
194
+ }
195
+
196
+ /**
197
+ * Check if currently recording
198
+ */
199
+ isRecording() {
200
+ return this.state === "recording";
201
+ }
202
+
203
+ /**
204
+ * Set the batch size context (for report metadata)
205
+ */
206
+ setBatchSize(size) {
207
+ this.batchSize = size;
208
+ }
209
+
210
+ /**
211
+ * Set the showRenderCount context (for report metadata)
212
+ */
213
+ setShowRenderCount(enabled) {
214
+ this.showRenderCount = enabled;
215
+ }
216
+
217
+ /**
218
+ * Start a new benchmark session
219
+ */
220
+ startSession(options) {
221
+ if (this.state === "recording") {
222
+ console.warn("[BenchmarkRecorder] Session already recording. Stop it first.");
223
+ return;
224
+ }
225
+
226
+ // Reset state
227
+ this.sessionId = generateSessionId(options.name);
228
+ this.sessionName = options.name;
229
+ this.sessionDescription = options.description;
230
+ this.verbose = options.verbose ?? false;
231
+ this.captureMemory = options.captureMemory ?? true;
232
+ this.batches = [];
233
+ this.overlayRenders = [];
234
+ this.marks = [];
235
+ this.measures = [];
236
+ this.activeMeasures.clear();
237
+
238
+ // Capture start memory
239
+ if (this.captureMemory) {
240
+ this.memoryStart = captureMemorySnapshot();
241
+ }
242
+
243
+ // Record start time
244
+ this.sessionStartTime = performance.now();
245
+
246
+ // Add start mark
247
+ this.marks.push({
248
+ name: "session_start",
249
+ startTime: 0,
250
+ detail: {
251
+ name: this.sessionName
252
+ }
253
+ });
254
+ this.state = "recording";
255
+ if (this.verbose) {
256
+ console.log(`[BenchmarkRecorder] Session started: ${this.sessionName}`);
257
+ console.log(` ID: ${this.sessionId}`);
258
+ }
259
+ this.notifyListeners("start");
260
+ }
261
+
262
+ /**
263
+ * Stop the current session and generate a report
264
+ */
265
+ stopSession() {
266
+ if (this.state !== "recording") {
267
+ console.warn("[BenchmarkRecorder] No active session to stop.");
268
+ return null;
269
+ }
270
+ const endTime = performance.now();
271
+ const duration = endTime - this.sessionStartTime;
272
+
273
+ // Add end mark
274
+ this.marks.push({
275
+ name: "session_end",
276
+ startTime: duration
277
+ });
278
+
279
+ // Add total measure
280
+ this.measures.push({
281
+ name: "session_total",
282
+ startTime: 0,
283
+ duration
284
+ });
285
+
286
+ // Capture end memory
287
+ if (this.captureMemory) {
288
+ this.memoryEnd = captureMemorySnapshot();
289
+ }
290
+
291
+ // Calculate memory delta
292
+ let memoryDelta = null;
293
+ if (this.memoryStart?.usedJSHeapSize != null && this.memoryEnd?.usedJSHeapSize != null) {
294
+ memoryDelta = this.memoryEnd.usedJSHeapSize - this.memoryStart.usedJSHeapSize;
295
+ }
296
+
297
+ // Compute aggregated stats
298
+ const stats = computeStats(this.batches, this.overlayRenders);
299
+
300
+ // Build report
301
+ const report = {
302
+ version: "1.0",
303
+ id: this.sessionId,
304
+ name: this.sessionName,
305
+ description: this.sessionDescription,
306
+ createdAt: Date.now(),
307
+ duration,
308
+ context: getBenchmarkContext(this.batchSize, this.showRenderCount),
309
+ batches: [...this.batches],
310
+ overlayRenders: [...this.overlayRenders],
311
+ marks: [...this.marks],
312
+ measures: [...this.measures],
313
+ stats,
314
+ memoryStart: this.memoryStart,
315
+ memoryEnd: this.memoryEnd,
316
+ memoryDelta
317
+ };
318
+ this.state = "stopped";
319
+ if (this.verbose) {
320
+ this.logReport(report);
321
+ }
322
+ this.notifyListeners("stop");
323
+ return report;
324
+ }
325
+
326
+ /**
327
+ * Record a batch of highlight updates
328
+ */
329
+ recordBatch(metrics) {
330
+ if (this.state !== "recording") return;
331
+ this.batches.push(metrics);
332
+
333
+ // Add a mark for this batch
334
+ this.marks.push({
335
+ name: `batch_${metrics.batchId}`,
336
+ startTime: performance.now() - this.sessionStartTime,
337
+ detail: {
338
+ nodesReceived: metrics.nodesReceived,
339
+ nodesProcessed: metrics.nodesInBatch,
340
+ totalTime: metrics.totalTime
341
+ }
342
+ });
343
+ if (this.verbose) {
344
+ console.log(`[BenchmarkRecorder] Batch ${metrics.batchId}: ` + `${metrics.nodesInBatch} nodes in ${metrics.totalTime.toFixed(1)}ms`);
345
+ }
346
+ this.notifyListeners("batch", metrics);
347
+ }
348
+
349
+ /**
350
+ * Record an overlay render
351
+ */
352
+ recordOverlayRender(highlightCount, renderTime) {
353
+ if (this.state !== "recording") return;
354
+ const metrics = {
355
+ highlightCount,
356
+ renderTime,
357
+ timestamp: performance.now()
358
+ };
359
+ this.overlayRenders.push(metrics);
360
+ this.marks.push({
361
+ name: "overlay_render",
362
+ startTime: performance.now() - this.sessionStartTime,
363
+ detail: {
364
+ highlightCount,
365
+ renderTime
366
+ }
367
+ });
368
+ if (this.verbose) {
369
+ console.log(`[BenchmarkRecorder] Overlay render: ${highlightCount} highlights in ${renderTime.toFixed(1)}ms`);
370
+ }
371
+ this.notifyListeners("overlay", metrics);
372
+ }
373
+
374
+ /**
375
+ * Add a custom mark at the current time
376
+ */
377
+ mark(name, detail) {
378
+ if (this.state !== "recording") return;
379
+ this.marks.push({
380
+ name,
381
+ startTime: performance.now() - this.sessionStartTime,
382
+ detail
383
+ });
384
+ if (this.verbose) {
385
+ console.log(`[BenchmarkRecorder] Mark: ${name}`);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Start a custom measure
391
+ */
392
+ startMeasure(name) {
393
+ if (this.state !== "recording") return;
394
+ this.activeMeasures.set(name, performance.now());
395
+ if (this.verbose) {
396
+ console.log(`[BenchmarkRecorder] Measure started: ${name}`);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * End a custom measure
402
+ */
403
+ endMeasure(name, detail) {
404
+ if (this.state !== "recording") return null;
405
+ const startTime = this.activeMeasures.get(name);
406
+ if (startTime === undefined) {
407
+ console.warn(`[BenchmarkRecorder] No active measure: ${name}`);
408
+ return null;
409
+ }
410
+ const endTime = performance.now();
411
+ const duration = endTime - startTime;
412
+ this.measures.push({
413
+ name,
414
+ startTime: startTime - this.sessionStartTime,
415
+ duration,
416
+ detail
417
+ });
418
+ this.activeMeasures.delete(name);
419
+ if (this.verbose) {
420
+ console.log(`[BenchmarkRecorder] Measure ended: ${name} = ${duration.toFixed(1)}ms`);
421
+ }
422
+ return duration;
423
+ }
424
+
425
+ /**
426
+ * Subscribe to benchmark events
427
+ */
428
+ subscribe(listener) {
429
+ this.listeners.add(listener);
430
+ return () => {
431
+ this.listeners.delete(listener);
432
+ };
433
+ }
434
+
435
+ /**
436
+ * Notify event listeners
437
+ */
438
+ notifyListeners(event, data) {
439
+ for (const listener of this.listeners) {
440
+ try {
441
+ listener(event, data);
442
+ } catch (error) {
443
+ console.error("[BenchmarkRecorder] Error in event listener:", error);
444
+ }
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Log a summary of the report
450
+ */
451
+ logReport(report) {
452
+ const {
453
+ stats,
454
+ memoryDelta
455
+ } = report;
456
+ console.log("\n╔══════════════════════════════════════════════════════════════╗");
457
+ console.log("║ BENCHMARK REPORT ║");
458
+ console.log("╠══════════════════════════════════════════════════════════════╣");
459
+ console.log(`║ Name: ${report.name.padEnd(55)}║`);
460
+ console.log(`║ ID: ${report.id.substring(0, 57).padEnd(57)}║`);
461
+ console.log(`║ Duration: ${report.duration.toFixed(1).padStart(8)}ms ║`);
462
+ console.log("╠══════════════════════════════════════════════════════════════╣");
463
+ console.log("║ BATCH STATS ║");
464
+ console.log(`║ Count: ${stats.batchCount.toString().padStart(6)} ║`);
465
+ console.log(`║ Nodes received: ${stats.totalNodesReceived.toString().padStart(8)} ║`);
466
+ console.log(`║ Nodes processed: ${stats.totalNodesProcessed.toString().padStart(7)} ║`);
467
+ console.log("╠══════════════════════════════════════════════════════════════╣");
468
+ console.log("║ TIMING (avg per batch) ║");
469
+ console.log(`║ Filter: ${stats.avgFilterTime.toFixed(1).padStart(8)}ms ║`);
470
+ console.log(`║ Measure: ${stats.avgMeasureTime.toFixed(1).padStart(7)}ms ← Primary bottleneck ║`);
471
+ console.log(`║ Track: ${stats.avgTrackTime.toFixed(1).padStart(9)}ms ║`);
472
+ console.log(`║ Callback: ${stats.avgCallbackTime.toFixed(1).padStart(6)}ms ║`);
473
+ console.log(`║ Total: ${stats.avgTotalTime.toFixed(1).padStart(9)}ms ║`);
474
+ console.log("╠══════════════════════════════════════════════════════════════╣");
475
+ console.log("║ PERCENTILES ║");
476
+ console.log(`║ P50: ${stats.p50TotalTime.toFixed(1).padStart(8)}ms ║`);
477
+ console.log(`║ P95: ${stats.p95TotalTime.toFixed(1).padStart(8)}ms ║`);
478
+ console.log(`║ P99: ${stats.p99TotalTime.toFixed(1).padStart(8)}ms ║`);
479
+ console.log("╠══════════════════════════════════════════════════════════════╣");
480
+ console.log("║ OVERLAY RENDERS ║");
481
+ console.log(`║ Avg time: ${stats.avgOverlayRenderTime.toFixed(1).padStart(7)}ms ║`);
482
+ console.log(`║ Avg highlights: ${stats.avgHighlightsPerRender.toFixed(0).padStart(5)} ║`);
483
+ if (memoryDelta != null) {
484
+ const deltaMB = (memoryDelta / 1024 / 1024).toFixed(2);
485
+ const sign = memoryDelta >= 0 ? "+" : "";
486
+ console.log("╠══════════════════════════════════════════════════════════════╣");
487
+ console.log("║ MEMORY ║");
488
+ console.log(`║ Delta: ${sign}${deltaMB.padStart(7)}MB ║`);
489
+ }
490
+ console.log("╚══════════════════════════════════════════════════════════════╝\n");
491
+ }
492
+ }
493
+
494
+ // Export singleton instance for convenience
495
+ exports.BenchmarkRecorder = BenchmarkRecorder;
496
+ const benchmarkRecorder = exports.benchmarkRecorder = new BenchmarkRecorder();
497
+ var _default = exports.default = BenchmarkRecorder;