@viewscript/renderer 0.1.0-202605140639

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 (89) hide show
  1. package/dist/ast/types.d.ts +403 -0
  2. package/dist/ast/types.js +33 -0
  3. package/dist/compiler/chunk-splitter.d.ts +98 -0
  4. package/dist/compiler/chunk-splitter.js +361 -0
  5. package/dist/index.d.ts +55 -0
  6. package/dist/index.js +17 -0
  7. package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
  8. package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
  9. package/dist/rasterizer/canvas-mapper.d.ts +280 -0
  10. package/dist/rasterizer/canvas-mapper.js +414 -0
  11. package/dist/rasterizer/error-distribution.d.ts +143 -0
  12. package/dist/rasterizer/error-distribution.js +231 -0
  13. package/dist/rasterizer/gradient-mapper.d.ts +223 -0
  14. package/dist/rasterizer/gradient-mapper.js +352 -0
  15. package/dist/rasterizer/topology-rounding.d.ts +151 -0
  16. package/dist/rasterizer/topology-rounding.js +347 -0
  17. package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
  18. package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
  19. package/dist/runtime/event-backpressure.d.ts +393 -0
  20. package/dist/runtime/event-backpressure.js +458 -0
  21. package/dist/runtime/render-loop.d.ts +277 -0
  22. package/dist/runtime/render-loop.js +435 -0
  23. package/dist/runtime/wasm-resource-manager.d.ts +122 -0
  24. package/dist/runtime/wasm-resource-manager.js +253 -0
  25. package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
  26. package/dist/runtime/wgpu-renderer-adapter.js +230 -0
  27. package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
  28. package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
  29. package/dist/semantic/semantic-translator.d.ts +229 -0
  30. package/dist/semantic/semantic-translator.js +398 -0
  31. package/package.json +28 -0
  32. package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
  33. package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
  34. package/playwright-report/index.html +90 -0
  35. package/playwright.config.ts +160 -0
  36. package/screenshot-chrome.png +0 -0
  37. package/screenshots/visual-demo-verification.png +0 -0
  38. package/screenshots/visual-demo.png +0 -0
  39. package/src/ast/types.ts +473 -0
  40. package/src/compiler/chunk-splitter.ts +534 -0
  41. package/src/index.ts +62 -0
  42. package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
  43. package/src/rasterizer/canvas-mapper.ts +677 -0
  44. package/src/rasterizer/error-distribution.ts +344 -0
  45. package/src/rasterizer/gradient-mapper.ts +563 -0
  46. package/src/rasterizer/topology-rounding.ts +499 -0
  47. package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
  48. package/src/runtime/event-backpressure.ts +622 -0
  49. package/src/runtime/render-loop.ts +660 -0
  50. package/src/runtime/wasm-resource-manager.ts +349 -0
  51. package/src/runtime/wgpu-renderer-adapter.ts +318 -0
  52. package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
  53. package/src/semantic/semantic-translator.ts +637 -0
  54. package/test-results/.last-run.json +4 -0
  55. package/tests/e2e/async-race.spec.ts +612 -0
  56. package/tests/e2e/bilayer-sync.spec.ts +405 -0
  57. package/tests/e2e/failures/.gitkeep +0 -0
  58. package/tests/e2e/fullstack.spec.ts +681 -0
  59. package/tests/e2e/g1-continuity.spec.ts +703 -0
  60. package/tests/e2e/golden/.gitkeep +0 -0
  61. package/tests/e2e/golden/conic-color-wheel.raw +0 -0
  62. package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
  63. package/tests/e2e/golden/conic-rotated.raw +0 -0
  64. package/tests/e2e/golden/conic-rotated.sha256 +1 -0
  65. package/tests/e2e/golden/linear-45deg.raw +0 -0
  66. package/tests/e2e/golden/linear-45deg.sha256 +1 -0
  67. package/tests/e2e/golden/linear-horizontal.raw +0 -0
  68. package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
  69. package/tests/e2e/golden/linear-multi-stop.raw +0 -0
  70. package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
  71. package/tests/e2e/golden/radial-circle-center.raw +0 -0
  72. package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
  73. package/tests/e2e/golden/radial-offset.raw +0 -0
  74. package/tests/e2e/golden/radial-offset.sha256 +1 -0
  75. package/tests/e2e/golden/tile-mirror.raw +0 -0
  76. package/tests/e2e/golden/tile-mirror.sha256 +1 -0
  77. package/tests/e2e/golden/tile-repeat.raw +0 -0
  78. package/tests/e2e/golden/tile-repeat.sha256 +1 -0
  79. package/tests/e2e/gradient-animation.spec.ts +606 -0
  80. package/tests/e2e/memory-stability.spec.ts +396 -0
  81. package/tests/e2e/path-topology.spec.ts +674 -0
  82. package/tests/e2e/performance-profile.spec.ts +501 -0
  83. package/tests/e2e/screenshot.spec.ts +60 -0
  84. package/tests/e2e/test-harness.html +1005 -0
  85. package/tests/e2e/text-layout.spec.ts +451 -0
  86. package/tests/e2e/visual-demo.html +340 -0
  87. package/tests/e2e/visual-regression.spec.ts +335 -0
  88. package/tsconfig.json +12 -0
  89. package/vitest.config.ts +8 -0
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Performance Profiling Tests (Jank & Backpressure Verification)
3
+ *
4
+ * This module verifies that the render loop maintains 60fps under
5
+ * extreme stress conditions using Chrome DevTools Protocol (CDP).
6
+ *
7
+ * ## Performance Targets
8
+ *
9
+ * | Metric | Target | Rationale |
10
+ * |---------------------------|-----------------|------------------------------|
11
+ * | Frame Time p99 | < 16.6ms | 60fps guarantee |
12
+ * | Forced Sync Layout | = 0 | No layout thrashing |
13
+ * | Event Coalesce Rate | > 90% | Backpressure working |
14
+ * | Constraint Solve Time p95 | < 10ms | Leave headroom for render |
15
+ *
16
+ * ## Stress Test Scenarios
17
+ *
18
+ * 1. Event Storm: 1000 mousemove events per frame
19
+ * 2. Complex Graph: 10,000 constraint nodes
20
+ * 3. Combined: Both simultaneously
21
+ */
22
+
23
+ import { test, expect, type Page, type CDPSession } from '@playwright/test';
24
+
25
+ // =============================================================================
26
+ // Performance Thresholds
27
+ // =============================================================================
28
+
29
+ const PERF_THRESHOLDS = {
30
+ /** 60fps = 16.666ms per frame. Allow up to 17ms to accommodate
31
+ * sub-millisecond rAF timestamp rounding on real 60Hz displays. */
32
+ FRAME_TIME_P99_MS: 17.0,
33
+
34
+ /** Zero tolerance for layout thrashing */
35
+ MAX_FORCED_SYNC_LAYOUT: 0,
36
+
37
+ /** Minimum event coalesce rate */
38
+ MIN_COALESCE_RATE: 0.9,
39
+
40
+ /** Constraint solver budget (leave 6ms for rendering) */
41
+ CONSTRAINT_SOLVE_P95_MS: 10,
42
+
43
+ /** Test duration */
44
+ STRESS_TEST_DURATION_MS: 5000,
45
+ };
46
+
47
+ // =============================================================================
48
+ // Types
49
+ // =============================================================================
50
+
51
+ interface PerformanceMetrics {
52
+ frameTimes: number[];
53
+ forcedSyncLayoutCount?: number;
54
+ totalEventsReceived: number;
55
+ totalEventsProcessed: number;
56
+ constraintSolveTimes: number[];
57
+ }
58
+
59
+ interface PerformanceReport {
60
+ frameTimeP50: number;
61
+ frameTimeP95: number;
62
+ frameTimeP99: number;
63
+ frameTimeMax: number;
64
+ forcedSyncLayoutCount: number;
65
+ coalesceRate: number;
66
+ constraintSolveP95: number;
67
+ passed: boolean;
68
+ failures: string[];
69
+ }
70
+
71
+ // =============================================================================
72
+ // Performance Tests
73
+ // =============================================================================
74
+
75
+ test.describe('Performance Profiling: Jank & Backpressure', () => {
76
+
77
+ /**
78
+ * Test: Backpressure under event storm
79
+ *
80
+ * Inject 1000 mousemove events per frame, verify coalescing.
81
+ */
82
+ test('backpressure coalesces 1000 events/frame to < 50', async ({ page }) => {
83
+ const cdp = await page.context().newCDPSession(page);
84
+ await setupTestPage(page);
85
+
86
+ // Enable performance tracing
87
+ await enablePerformanceTracing(cdp);
88
+
89
+ // Start the event storm
90
+ const metrics = await runEventStormTest(page, {
91
+ eventsPerFrame: 1000,
92
+ durationMs: PERF_THRESHOLDS.STRESS_TEST_DURATION_MS,
93
+ });
94
+
95
+ // Stop tracing and collect
96
+ await disablePerformanceTracing(cdp);
97
+
98
+ // Analyze results
99
+ const report = analyzeMetrics(metrics);
100
+
101
+ console.log('=== Backpressure Test Results ===');
102
+ console.log(`Events received: ${metrics.totalEventsReceived}`);
103
+ console.log(`Events processed: ${metrics.totalEventsProcessed}`);
104
+ console.log(`Coalesce rate: ${(report.coalesceRate * 100).toFixed(1)}%`);
105
+
106
+ // Assertions
107
+ expect(report.coalesceRate).toBeGreaterThanOrEqual(PERF_THRESHOLDS.MIN_COALESCE_RATE);
108
+ });
109
+
110
+ /**
111
+ * Test: 10,000 node constraint graph at 60fps
112
+ *
113
+ * Render and animate a massive constraint graph.
114
+ */
115
+ test('maintains 60fps with 10,000 constraint nodes', async ({ page }) => {
116
+ const cdp = await page.context().newCDPSession(page);
117
+ await setupTestPage(page);
118
+
119
+ await enablePerformanceTracing(cdp);
120
+
121
+ // Generate and render massive constraint graph
122
+ const metrics = await runComplexGraphTest(page, {
123
+ nodeCount: 10000,
124
+ durationMs: PERF_THRESHOLDS.STRESS_TEST_DURATION_MS,
125
+ });
126
+
127
+ await disablePerformanceTracing(cdp);
128
+
129
+ const report = analyzeMetrics(metrics);
130
+
131
+ console.log('=== Complex Graph Test Results ===');
132
+ console.log(`Frame Time p50: ${report.frameTimeP50.toFixed(2)}ms`);
133
+ console.log(`Frame Time p95: ${report.frameTimeP95.toFixed(2)}ms`);
134
+ console.log(`Frame Time p99: ${report.frameTimeP99.toFixed(2)}ms`);
135
+ console.log(`Frame Time max: ${report.frameTimeMax.toFixed(2)}ms`);
136
+ console.log(`Constraint Solve p95: ${report.constraintSolveP95.toFixed(2)}ms`);
137
+
138
+ // Assertions
139
+ expect(report.frameTimeP99).toBeLessThan(PERF_THRESHOLDS.FRAME_TIME_P99_MS);
140
+ expect(report.constraintSolveP95).toBeLessThan(PERF_THRESHOLDS.CONSTRAINT_SOLVE_P95_MS);
141
+ });
142
+
143
+ /**
144
+ * Test: Zero forced synchronous layouts
145
+ *
146
+ * Verify that our atomic render loop never causes layout thrashing.
147
+ */
148
+ test('zero forced synchronous layouts during rendering', async ({ page }) => {
149
+ const cdp = await page.context().newCDPSession(page);
150
+ await setupTestPage(page);
151
+
152
+ // Enable Layout Instability detection
153
+ await cdp.send('Performance.enable');
154
+
155
+ const metrics = await runCombinedStressTest(page, {
156
+ eventsPerFrame: 100,
157
+ nodeCount: 1000,
158
+ durationMs: PERF_THRESHOLDS.STRESS_TEST_DURATION_MS,
159
+ });
160
+
161
+ // Get layout metrics from CDP
162
+ const layoutMetrics = await getLayoutMetrics(cdp);
163
+
164
+ console.log('=== Layout Thrashing Test Results ===');
165
+ console.log(`Forced Sync Layouts: ${layoutMetrics.forcedSyncLayoutCount}`);
166
+
167
+ // CRITICAL: Zero tolerance
168
+ expect(layoutMetrics.forcedSyncLayoutCount).toBe(PERF_THRESHOLDS.MAX_FORCED_SYNC_LAYOUT);
169
+ });
170
+
171
+ /**
172
+ * Test: Combined stress (event storm + complex graph)
173
+ *
174
+ * The ultimate stress test: everything at once.
175
+ */
176
+ test('combined stress maintains performance invariants', async ({ page }) => {
177
+ const cdp = await page.context().newCDPSession(page);
178
+ await setupTestPage(page);
179
+
180
+ await enablePerformanceTracing(cdp);
181
+
182
+ const metrics = await runCombinedStressTest(page, {
183
+ eventsPerFrame: 1000,
184
+ nodeCount: 10000,
185
+ durationMs: PERF_THRESHOLDS.STRESS_TEST_DURATION_MS,
186
+ });
187
+
188
+ await disablePerformanceTracing(cdp);
189
+
190
+ const report = analyzeMetrics(metrics);
191
+
192
+ console.log('=== Combined Stress Test Results ===');
193
+ console.log(`Frame Time p99: ${report.frameTimeP99.toFixed(2)}ms`);
194
+ console.log(`Coalesce Rate: ${(report.coalesceRate * 100).toFixed(1)}%`);
195
+ console.log(`Forced Sync Layouts: ${report.forcedSyncLayoutCount}`);
196
+ console.log(`Test Passed: ${report.passed ? 'YES' : 'NO'}`);
197
+ if (!report.passed) {
198
+ console.log(`Failures: ${report.failures.join(', ')}`);
199
+ }
200
+
201
+ // All invariants must hold
202
+ expect(report.passed).toBe(true);
203
+ });
204
+ });
205
+
206
+ // =============================================================================
207
+ // CDP Helpers
208
+ // =============================================================================
209
+
210
+ async function enablePerformanceTracing(cdp: CDPSession): Promise<void> {
211
+ await cdp.send('Performance.enable');
212
+ await cdp.send('Tracing.start', {
213
+ categories: 'devtools.timeline,v8.execute,blink.user_timing',
214
+ options: 'sampling-frequency=10000',
215
+ });
216
+ }
217
+
218
+ async function disablePerformanceTracing(cdp: CDPSession): Promise<void> {
219
+ await cdp.send('Tracing.end');
220
+ }
221
+
222
+ async function getLayoutMetrics(cdp: CDPSession): Promise<{ forcedSyncLayoutCount: number }> {
223
+ const result = await cdp.send('Performance.getMetrics');
224
+ const metrics = result.metrics;
225
+
226
+ // CRITICAL: Use correct metric for forced synchronous layouts
227
+ // LayoutCount = total layouts (includes async)
228
+ // ForcedStyleAndLayoutDuration > 0 indicates forced sync layouts occurred
229
+ // RecalcStyleCount can also indicate style thrashing
230
+ const forcedStyleDuration = metrics.find(m => m.name === 'ForcedStyleAndLayoutDuration');
231
+ const layoutDuration = metrics.find(m => m.name === 'LayoutDuration');
232
+
233
+ // Heuristic: If forced style duration is significant (>1ms), we have forced layouts
234
+ // More accurate than LayoutCount which doesn't distinguish sync vs async
235
+ const hasForcedLayouts = (forcedStyleDuration?.value ?? 0) > 0.001;
236
+
237
+ return {
238
+ forcedSyncLayoutCount: hasForcedLayouts ? 1 : 0,
239
+ };
240
+ }
241
+
242
+ // =============================================================================
243
+ // Test Scenarios
244
+ // =============================================================================
245
+
246
+ async function setupTestPage(page: Page): Promise<void> {
247
+ await page.goto('/test-harness.html');
248
+ await page.waitForFunction(() => (window as any).__VS_RENDERER_READY__ === true, {
249
+ timeout: 10000,
250
+ });
251
+
252
+ // Initialize metrics collection
253
+ await page.evaluate(() => {
254
+ (window as any).__VS_PERF_METRICS__ = {
255
+ frameTimes: [],
256
+ constraintSolveTimes: [],
257
+ totalEventsReceived: 0,
258
+ totalEventsProcessed: 0,
259
+ forcedSyncLayoutCount: 0,
260
+ };
261
+
262
+ // Track frame-to-frame timing (actual frame duration)
263
+ // This measures the real interval between animation frames,
264
+ // NOT just JS execution time within a frame
265
+ let lastFrameTime: number | null = null;
266
+
267
+ const measureFrameTime = (timestamp: number) => {
268
+ if (lastFrameTime !== null) {
269
+ const frameInterval = timestamp - lastFrameTime;
270
+ (window as any).__VS_PERF_METRICS__.frameTimes.push(frameInterval);
271
+ }
272
+ lastFrameTime = timestamp;
273
+ requestAnimationFrame(measureFrameTime);
274
+ };
275
+ requestAnimationFrame(measureFrameTime);
276
+
277
+ // Hook into render loop for constraint solve timing only
278
+ const originalTick = (window as any).__VS_RENDERER__.tick;
279
+ (window as any).__VS_RENDERER__.tick = function(ts: number) {
280
+ const start = performance.now();
281
+ originalTick.call(this, ts);
282
+ const elapsed = performance.now() - start;
283
+ (window as any).__VS_PERF_METRICS__.constraintSolveTimes.push(elapsed);
284
+ };
285
+ });
286
+ }
287
+
288
+ async function runEventStormTest(
289
+ page: Page,
290
+ config: { eventsPerFrame: number; durationMs: number },
291
+ ): Promise<PerformanceMetrics> {
292
+ await page.evaluate(async (cfg) => {
293
+ const startTime = performance.now();
294
+ const metrics = (window as any).__VS_PERF_METRICS__;
295
+
296
+ const runFrame = () => {
297
+ // Inject N synthetic mousemove events
298
+ for (let i = 0; i < cfg.eventsPerFrame; i++) {
299
+ const event = new MouseEvent('mousemove', {
300
+ clientX: Math.random() * 800,
301
+ clientY: Math.random() * 600,
302
+ });
303
+ document.dispatchEvent(event);
304
+ metrics.totalEventsReceived++;
305
+ }
306
+
307
+ if (performance.now() - startTime < cfg.durationMs) {
308
+ requestAnimationFrame(runFrame);
309
+ }
310
+ };
311
+
312
+ requestAnimationFrame(runFrame);
313
+
314
+ // Wait for test completion
315
+ await new Promise(resolve => setTimeout(resolve, cfg.durationMs + 100));
316
+
317
+ // Get processed count from EventBuffer
318
+ metrics.totalEventsProcessed = (window as any).__VS_RENDERER__.getProcessedEventCount();
319
+ }, config);
320
+
321
+ return await page.evaluate(() => (window as any).__VS_PERF_METRICS__);
322
+ }
323
+
324
+ async function runComplexGraphTest(
325
+ page: Page,
326
+ config: { nodeCount: number; durationMs: number },
327
+ ): Promise<PerformanceMetrics> {
328
+ await page.evaluate(async (cfg) => {
329
+ const renderer = (window as any).__VS_RENDERER__;
330
+ const metrics = (window as any).__VS_PERF_METRICS__;
331
+
332
+ // Generate massive constraint graph
333
+ const entities = [];
334
+ const constraints = [];
335
+
336
+ for (let i = 0; i < cfg.nodeCount; i++) {
337
+ entities.push({
338
+ id: i,
339
+ type: 'rect',
340
+ bounds: {
341
+ x: (i % 100) * 10,
342
+ y: Math.floor(i / 100) * 10,
343
+ width: 8,
344
+ height: 8,
345
+ },
346
+ fill: `hsl(${(i * 137) % 360}, 70%, 50%)`,
347
+ });
348
+
349
+ // Create constraint chains
350
+ if (i > 0) {
351
+ constraints.push({
352
+ type: 'adjacent',
353
+ a: { entityId: i - 1, edge: 'right' },
354
+ b: { entityId: i, edge: 'left' },
355
+ });
356
+ }
357
+ }
358
+
359
+ // Render initial state
360
+ renderer.render({ entities, constraints });
361
+
362
+ // Animate for duration
363
+ const startTime = performance.now();
364
+
365
+ const animate = () => {
366
+ const t = (performance.now() - startTime) / 1000;
367
+
368
+ // Update T-vector to animate positions
369
+ const solveStart = performance.now();
370
+ renderer.updateTVector(t);
371
+ metrics.constraintSolveTimes.push(performance.now() - solveStart);
372
+
373
+ if (performance.now() - startTime < cfg.durationMs) {
374
+ requestAnimationFrame(animate);
375
+ }
376
+ };
377
+
378
+ requestAnimationFrame(animate);
379
+
380
+ await new Promise(resolve => setTimeout(resolve, cfg.durationMs + 100));
381
+ }, config);
382
+
383
+ return await page.evaluate(() => (window as any).__VS_PERF_METRICS__);
384
+ }
385
+
386
+ async function runCombinedStressTest(
387
+ page: Page,
388
+ config: { eventsPerFrame: number; nodeCount: number; durationMs: number },
389
+ ): Promise<PerformanceMetrics> {
390
+ await page.evaluate(async (cfg) => {
391
+ const renderer = (window as any).__VS_RENDERER__;
392
+ const metrics = (window as any).__VS_PERF_METRICS__;
393
+
394
+ // Generate constraint graph
395
+ const entities = [];
396
+ for (let i = 0; i < cfg.nodeCount; i++) {
397
+ entities.push({
398
+ id: i,
399
+ type: 'rect',
400
+ bounds: {
401
+ x: (i % 100) * 8,
402
+ y: Math.floor(i / 100) * 8,
403
+ width: 6,
404
+ height: 6,
405
+ },
406
+ fill: `hsl(${(i * 137) % 360}, 70%, 50%)`,
407
+ });
408
+ }
409
+ renderer.render({ entities, constraints: [] });
410
+
411
+ const startTime = performance.now();
412
+
413
+ const runFrame = () => {
414
+ // Event storm
415
+ for (let i = 0; i < cfg.eventsPerFrame; i++) {
416
+ const event = new MouseEvent('mousemove', {
417
+ clientX: Math.random() * 800,
418
+ clientY: Math.random() * 600,
419
+ });
420
+ document.dispatchEvent(event);
421
+ metrics.totalEventsReceived++;
422
+ }
423
+
424
+ // Constraint solving
425
+ const t = (performance.now() - startTime) / 1000;
426
+ const solveStart = performance.now();
427
+ renderer.updateTVector(t);
428
+ metrics.constraintSolveTimes.push(performance.now() - solveStart);
429
+
430
+ if (performance.now() - startTime < cfg.durationMs) {
431
+ requestAnimationFrame(runFrame);
432
+ }
433
+ };
434
+
435
+ requestAnimationFrame(runFrame);
436
+
437
+ await new Promise(resolve => setTimeout(resolve, cfg.durationMs + 100));
438
+
439
+ // Safely get processed event count (may not be implemented)
440
+ const processedCount = typeof renderer.getProcessedEventCount === 'function'
441
+ ? renderer.getProcessedEventCount()
442
+ : metrics.totalEventsReceived; // Fallback: assume all processed (no coalescing)
443
+ metrics.totalEventsProcessed = processedCount;
444
+ }, config);
445
+
446
+ return await page.evaluate(() => (window as any).__VS_PERF_METRICS__);
447
+ }
448
+
449
+ // =============================================================================
450
+ // Analysis
451
+ // =============================================================================
452
+
453
+ function analyzeMetrics(metrics: PerformanceMetrics): PerformanceReport {
454
+ const failures: string[] = [];
455
+
456
+ // Sort for percentile calculation
457
+ const sortedFrameTimes = [...metrics.frameTimes].sort((a, b) => a - b);
458
+ const sortedConstraintTimes = [...metrics.constraintSolveTimes].sort((a, b) => a - b);
459
+
460
+ const percentile = (arr: number[], p: number): number => {
461
+ if (arr.length === 0) return 0;
462
+ const idx = Math.ceil(arr.length * p) - 1;
463
+ return arr[Math.max(0, idx)];
464
+ };
465
+
466
+ const frameTimeP50 = percentile(sortedFrameTimes, 0.50);
467
+ const frameTimeP95 = percentile(sortedFrameTimes, 0.95);
468
+ const frameTimeP99 = percentile(sortedFrameTimes, 0.99);
469
+ const frameTimeMax = sortedFrameTimes[sortedFrameTimes.length - 1] ?? 0;
470
+ const constraintSolveP95 = percentile(sortedConstraintTimes, 0.95);
471
+
472
+ const coalesceRate = metrics.totalEventsReceived > 0
473
+ ? 1 - (metrics.totalEventsProcessed / metrics.totalEventsReceived)
474
+ : 1;
475
+
476
+ // Check thresholds
477
+ if (frameTimeP99 >= PERF_THRESHOLDS.FRAME_TIME_P99_MS) {
478
+ failures.push(`Frame time p99 (${frameTimeP99.toFixed(2)}ms) >= ${PERF_THRESHOLDS.FRAME_TIME_P99_MS}ms`);
479
+ }
480
+ if (metrics.forcedSyncLayoutCount > PERF_THRESHOLDS.MAX_FORCED_SYNC_LAYOUT) {
481
+ failures.push(`Forced sync layouts (${metrics.forcedSyncLayoutCount}) > 0`);
482
+ }
483
+ if (coalesceRate < PERF_THRESHOLDS.MIN_COALESCE_RATE) {
484
+ failures.push(`Coalesce rate (${(coalesceRate * 100).toFixed(1)}%) < 90%`);
485
+ }
486
+ if (constraintSolveP95 >= PERF_THRESHOLDS.CONSTRAINT_SOLVE_P95_MS) {
487
+ failures.push(`Constraint solve p95 (${constraintSolveP95.toFixed(2)}ms) >= ${PERF_THRESHOLDS.CONSTRAINT_SOLVE_P95_MS}ms`);
488
+ }
489
+
490
+ return {
491
+ frameTimeP50,
492
+ frameTimeP95,
493
+ frameTimeP99,
494
+ frameTimeMax,
495
+ forcedSyncLayoutCount: metrics.forcedSyncLayoutCount,
496
+ coalesceRate,
497
+ constraintSolveP95,
498
+ passed: failures.length === 0,
499
+ failures,
500
+ };
501
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Visual Demo Screenshot Test
3
+ *
4
+ * Takes a screenshot of the ViewScript renderer displaying IR constructed
5
+ * through the standard pipeline.
6
+ */
7
+
8
+ import { test, expect } from '@playwright/test';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+ import { existsSync, mkdirSync, statSync } from 'fs';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ test.describe('Visual Demo Screenshot', () => {
17
+ test('capture ViewScript rendering', async ({ page, baseURL }) => {
18
+ // Navigate to the visual demo via webServer
19
+ await page.goto(`${baseURL}/visual-demo.html`);
20
+
21
+ // Wait for renderer to initialize
22
+ await page.waitForFunction(() => window.__VS_DEMO_READY__ === true, {
23
+ timeout: 5000
24
+ });
25
+
26
+ // Wait a bit for animation to reach a good frame
27
+ await page.waitForTimeout(500);
28
+
29
+ // Take screenshot
30
+ const screenshotPath = join(__dirname, '../../screenshots/visual-demo.png');
31
+
32
+ // Ensure screenshots directory exists
33
+ const screenshotDir = dirname(screenshotPath);
34
+ if (!existsSync(screenshotDir)) {
35
+ mkdirSync(screenshotDir, { recursive: true });
36
+ }
37
+
38
+ await page.screenshot({
39
+ path: screenshotPath,
40
+ fullPage: false,
41
+ clip: { x: 0, y: 0, width: 800, height: 600 }
42
+ });
43
+
44
+ console.log(`Screenshot saved to: ${screenshotPath}`);
45
+
46
+ // Verify screenshot was created
47
+ expect(existsSync(screenshotPath)).toBe(true);
48
+
49
+ // Verify file size is reasonable (not empty)
50
+ const stats = statSync(screenshotPath);
51
+ expect(stats.size).toBeGreaterThan(1000); // At least 1KB
52
+ });
53
+ });
54
+
55
+ // Extend Window interface for TypeScript
56
+ declare global {
57
+ interface Window {
58
+ __VS_DEMO_READY__?: boolean;
59
+ }
60
+ }