@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.
- package/dist/ast/types.d.ts +403 -0
- package/dist/ast/types.js +33 -0
- package/dist/compiler/chunk-splitter.d.ts +98 -0
- package/dist/compiler/chunk-splitter.js +361 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +17 -0
- package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
- package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
- package/dist/rasterizer/canvas-mapper.d.ts +280 -0
- package/dist/rasterizer/canvas-mapper.js +414 -0
- package/dist/rasterizer/error-distribution.d.ts +143 -0
- package/dist/rasterizer/error-distribution.js +231 -0
- package/dist/rasterizer/gradient-mapper.d.ts +223 -0
- package/dist/rasterizer/gradient-mapper.js +352 -0
- package/dist/rasterizer/topology-rounding.d.ts +151 -0
- package/dist/rasterizer/topology-rounding.js +347 -0
- package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
- package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
- package/dist/runtime/event-backpressure.d.ts +393 -0
- package/dist/runtime/event-backpressure.js +458 -0
- package/dist/runtime/render-loop.d.ts +277 -0
- package/dist/runtime/render-loop.js +435 -0
- package/dist/runtime/wasm-resource-manager.d.ts +122 -0
- package/dist/runtime/wasm-resource-manager.js +253 -0
- package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
- package/dist/runtime/wgpu-renderer-adapter.js +230 -0
- package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
- package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
- package/dist/semantic/semantic-translator.d.ts +229 -0
- package/dist/semantic/semantic-translator.js +398 -0
- package/package.json +28 -0
- package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
- package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
- package/playwright-report/index.html +90 -0
- package/playwright.config.ts +160 -0
- package/screenshot-chrome.png +0 -0
- package/screenshots/visual-demo-verification.png +0 -0
- package/screenshots/visual-demo.png +0 -0
- package/src/ast/types.ts +473 -0
- package/src/compiler/chunk-splitter.ts +534 -0
- package/src/index.ts +62 -0
- package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
- package/src/rasterizer/canvas-mapper.ts +677 -0
- package/src/rasterizer/error-distribution.ts +344 -0
- package/src/rasterizer/gradient-mapper.ts +563 -0
- package/src/rasterizer/topology-rounding.ts +499 -0
- package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
- package/src/runtime/event-backpressure.ts +622 -0
- package/src/runtime/render-loop.ts +660 -0
- package/src/runtime/wasm-resource-manager.ts +349 -0
- package/src/runtime/wgpu-renderer-adapter.ts +318 -0
- package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
- package/src/semantic/semantic-translator.ts +637 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/async-race.spec.ts +612 -0
- package/tests/e2e/bilayer-sync.spec.ts +405 -0
- package/tests/e2e/failures/.gitkeep +0 -0
- package/tests/e2e/fullstack.spec.ts +681 -0
- package/tests/e2e/g1-continuity.spec.ts +703 -0
- package/tests/e2e/golden/.gitkeep +0 -0
- package/tests/e2e/golden/conic-color-wheel.raw +0 -0
- package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
- package/tests/e2e/golden/conic-rotated.raw +0 -0
- package/tests/e2e/golden/conic-rotated.sha256 +1 -0
- package/tests/e2e/golden/linear-45deg.raw +0 -0
- package/tests/e2e/golden/linear-45deg.sha256 +1 -0
- package/tests/e2e/golden/linear-horizontal.raw +0 -0
- package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
- package/tests/e2e/golden/linear-multi-stop.raw +0 -0
- package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
- package/tests/e2e/golden/radial-circle-center.raw +0 -0
- package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
- package/tests/e2e/golden/radial-offset.raw +0 -0
- package/tests/e2e/golden/radial-offset.sha256 +1 -0
- package/tests/e2e/golden/tile-mirror.raw +0 -0
- package/tests/e2e/golden/tile-mirror.sha256 +1 -0
- package/tests/e2e/golden/tile-repeat.raw +0 -0
- package/tests/e2e/golden/tile-repeat.sha256 +1 -0
- package/tests/e2e/gradient-animation.spec.ts +606 -0
- package/tests/e2e/memory-stability.spec.ts +396 -0
- package/tests/e2e/path-topology.spec.ts +674 -0
- package/tests/e2e/performance-profile.spec.ts +501 -0
- package/tests/e2e/screenshot.spec.ts +60 -0
- package/tests/e2e/test-harness.html +1005 -0
- package/tests/e2e/text-layout.spec.ts +451 -0
- package/tests/e2e/visual-demo.html +340 -0
- package/tests/e2e/visual-regression.spec.ts +335 -0
- package/tsconfig.json +12 -0
- 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
|
+
}
|