@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,396 @@
1
+ /**
2
+ * Memory Stability E2E Tests
3
+ *
4
+ * This module verifies that the WASM Resource Manager correctly prevents
5
+ * memory leaks during sustained HMR operations.
6
+ *
7
+ * ## Test Strategy
8
+ *
9
+ * 1. Measure baseline WASM heap size
10
+ * 2. Simulate 10,000 HMR patches (entity add/remove cycles)
11
+ * 3. Force garbage collection
12
+ * 4. Flush FinalizationRegistry cleanup queue
13
+ * 5. Measure final WASM heap size
14
+ * 6. Assert: |final - baseline| < 1MB
15
+ *
16
+ * ## Why This Matters
17
+ *
18
+ * Without explicit WASM resource management, each HMR patch that creates
19
+ * GPU objects (PaintSpec, PathEntity) would leak memory. Over a dev session
20
+ * with thousands of hot reloads, this could exhaust available memory.
21
+ */
22
+
23
+ import { test, expect, type Page, type CDPSession } from '@playwright/test';
24
+
25
+ // =============================================================================
26
+ // Configuration
27
+ // =============================================================================
28
+
29
+ const CONFIG = {
30
+ /** Number of HMR patch cycles to simulate */
31
+ HMR_CYCLES: 10000,
32
+
33
+ /** Entities per patch (add then remove) */
34
+ ENTITIES_PER_PATCH: 5,
35
+
36
+ /** Maximum allowed heap growth (bytes) */
37
+ MAX_HEAP_GROWTH_BYTES: 1024 * 1024, // 1MB
38
+
39
+ /** Delay between patches (ms) - 0 for stress test */
40
+ PATCH_DELAY_MS: 0,
41
+
42
+ /** GC attempts before final measurement */
43
+ GC_ATTEMPTS: 3,
44
+ };
45
+
46
+ // =============================================================================
47
+ // Types
48
+ // =============================================================================
49
+
50
+ interface MemoryMetrics {
51
+ jsHeapSizeUsed: number;
52
+ jsHeapSizeTotal: number;
53
+ wasmHeapEstimate: number;
54
+ timestamp: number;
55
+ }
56
+
57
+ interface TestResult {
58
+ baselineMemory: MemoryMetrics;
59
+ finalMemory: MemoryMetrics;
60
+ heapGrowth: number;
61
+ resourceStats: {
62
+ allocated: number;
63
+ released: number;
64
+ leaked: number;
65
+ };
66
+ passed: boolean;
67
+ }
68
+
69
+ // =============================================================================
70
+ // Tests
71
+ // =============================================================================
72
+
73
+ test.describe('Memory Stability: WASM Resource Management', () => {
74
+ test.setTimeout(300000); // 5 minutes for stress test
75
+
76
+ /**
77
+ * Core stability test: 10,000 HMR cycles without memory leak.
78
+ */
79
+ test('maintains stable memory through 10,000 HMR cycles', async ({ page }) => {
80
+ const cdp = await page.context().newCDPSession(page);
81
+ await setupTestPage(page);
82
+
83
+ // Enable performance instrumentation
84
+ await cdp.send('Performance.enable');
85
+
86
+ // Phase 1: Measure baseline
87
+ await forceGC(cdp);
88
+ const baseline = await measureMemory(page, cdp);
89
+ console.log(`[Baseline] JS Heap: ${formatBytes(baseline.jsHeapSizeUsed)}`);
90
+
91
+ // Phase 2: Run HMR stress test
92
+ console.log(`[Stress Test] Running ${CONFIG.HMR_CYCLES} HMR cycles...`);
93
+ const resourceStats = await runHMRStressTest(page, CONFIG.HMR_CYCLES);
94
+
95
+ // Phase 3: Force cleanup
96
+ await forceGC(cdp);
97
+ await flushFinalizationRegistry(page);
98
+ await forceGC(cdp);
99
+
100
+ // Phase 4: Measure final
101
+ const final = await measureMemory(page, cdp);
102
+ console.log(`[Final] JS Heap: ${formatBytes(final.jsHeapSizeUsed)}`);
103
+
104
+ // Phase 5: Calculate and assert
105
+ const heapGrowth = final.jsHeapSizeUsed - baseline.jsHeapSizeUsed;
106
+ const result: TestResult = {
107
+ baselineMemory: baseline,
108
+ finalMemory: final,
109
+ heapGrowth,
110
+ resourceStats,
111
+ passed: heapGrowth < CONFIG.MAX_HEAP_GROWTH_BYTES,
112
+ };
113
+
114
+ console.log('=== Memory Stability Report ===');
115
+ console.log(`Baseline: ${formatBytes(baseline.jsHeapSizeUsed)}`);
116
+ console.log(`Final: ${formatBytes(final.jsHeapSizeUsed)}`);
117
+ console.log(`Growth: ${formatBytes(heapGrowth)}`);
118
+ console.log(`Allocated: ${resourceStats.allocated}`);
119
+ console.log(`Released: ${resourceStats.released}`);
120
+ console.log(`Leaked: ${resourceStats.leaked}`);
121
+ console.log(`Limit: ${formatBytes(CONFIG.MAX_HEAP_GROWTH_BYTES)}`);
122
+ console.log(`Result: ${result.passed ? 'PASS' : 'FAIL'}`);
123
+
124
+ // Assertions
125
+ expect(heapGrowth).toBeLessThan(CONFIG.MAX_HEAP_GROWTH_BYTES);
126
+ expect(resourceStats.leaked).toBe(0);
127
+ });
128
+
129
+ /**
130
+ * Verify FinalizationRegistry actually triggers cleanup.
131
+ */
132
+ test('FinalizationRegistry releases orphaned resources', async ({ page }) => {
133
+ await setupTestPage(page);
134
+
135
+ // Create resources without explicit release
136
+ const orphanCount = await page.evaluate(() => {
137
+ const manager = (window as any).__VS_RESOURCE_MANAGER__;
138
+ const mockResources: any[] = [];
139
+
140
+ // Create 100 mock WASM resources
141
+ for (let i = 0; i < 100; i++) {
142
+ const mock = {
143
+ deleted: false,
144
+ delete() { this.deleted = true; },
145
+ isDeleted() { return this.deleted; },
146
+ };
147
+ manager.register(mock, 'paint', i);
148
+ mockResources.push(mock);
149
+ }
150
+
151
+ // Clear references (make them GC-eligible)
152
+ mockResources.length = 0;
153
+
154
+ return manager.getStats().currentActive;
155
+ });
156
+
157
+ // Force GC
158
+ const cdp = await page.context().newCDPSession(page);
159
+ await forceGC(cdp);
160
+
161
+ // Flush cleanup queue
162
+ await flushFinalizationRegistry(page);
163
+
164
+ // Check that resources were cleaned up
165
+ const afterCleanup = await page.evaluate(() => {
166
+ return (window as any).__VS_RESOURCE_MANAGER__.getStats().currentActive;
167
+ });
168
+
169
+ console.log(`Orphaned: ${orphanCount}, After cleanup: ${afterCleanup}`);
170
+
171
+ // Some resources may still be pending GC, but count should decrease
172
+ expect(afterCleanup).toBeLessThan(orphanCount);
173
+ });
174
+
175
+ /**
176
+ * Verify explicit release works correctly.
177
+ */
178
+ test('explicit release immediately frees resources', async ({ page }) => {
179
+ await setupTestPage(page);
180
+
181
+ const result = await page.evaluate(() => {
182
+ const manager = (window as any).__VS_RESOURCE_MANAGER__;
183
+
184
+ // Create and explicitly release
185
+ const tokens: symbol[] = [];
186
+ for (let i = 0; i < 50; i++) {
187
+ const mock = {
188
+ deleted: false,
189
+ delete() { this.deleted = true; },
190
+ isDeleted() { return this.deleted; },
191
+ };
192
+ tokens.push(manager.register(mock, 'path', i));
193
+ }
194
+
195
+ const beforeRelease = manager.getStats().currentActive;
196
+
197
+ // Release all
198
+ for (const token of tokens) {
199
+ manager.release(token);
200
+ }
201
+
202
+ const afterRelease = manager.getStats().currentActive;
203
+
204
+ return { beforeRelease, afterRelease };
205
+ });
206
+
207
+ expect(result.beforeRelease).toBe(50);
208
+ expect(result.afterRelease).toBe(0);
209
+ });
210
+
211
+ /**
212
+ * Verify entity-based batch release.
213
+ */
214
+ test('releaseByEntity cleans up all associated resources', async ({ page }) => {
215
+ await setupTestPage(page);
216
+
217
+ const result = await page.evaluate(() => {
218
+ const manager = (window as any).__VS_RESOURCE_MANAGER__;
219
+
220
+ // Create resources for entity 42
221
+ for (let i = 0; i < 10; i++) {
222
+ const mock = { delete() {}, isDeleted() { return false; } };
223
+ manager.register(mock, 'paint', 42);
224
+ }
225
+
226
+ // Create resources for entity 99
227
+ for (let i = 0; i < 10; i++) {
228
+ const mock = { delete() {}, isDeleted() { return false; } };
229
+ manager.register(mock, 'path', 99);
230
+ }
231
+
232
+ const before = manager.getStats().currentActive;
233
+
234
+ // Release only entity 42
235
+ const released = manager.releaseByEntity(42);
236
+
237
+ const after = manager.getStats().currentActive;
238
+
239
+ return { before, after, released };
240
+ });
241
+
242
+ expect(result.before).toBe(20);
243
+ expect(result.released).toBe(10);
244
+ expect(result.after).toBe(10);
245
+ });
246
+ });
247
+
248
+ // =============================================================================
249
+ // Helper Functions
250
+ // =============================================================================
251
+
252
+ async function setupTestPage(page: Page): Promise<void> {
253
+ await page.goto('/test-harness.html', { waitUntil: 'networkidle' });
254
+ await page.waitForFunction(() => (window as any).__VS_RENDERER_READY__ === true);
255
+
256
+ // Initialize resource manager on page
257
+ await page.evaluate(() => {
258
+ // Simplified resource manager for testing
259
+ class TestResourceManager {
260
+ private resources = new Map<symbol, { ref: WeakRef<any>; entry: any }>();
261
+ private registry: FinalizationRegistry<{ token: symbol }>;
262
+ private cleanupQueue: symbol[] = [];
263
+ private stats = { allocated: 0, released: 0 };
264
+
265
+ constructor() {
266
+ this.registry = new FinalizationRegistry(({ token }) => {
267
+ this.cleanupQueue.push(token);
268
+ });
269
+ }
270
+
271
+ register(resource: any, type: string, entityId: number | null) {
272
+ const token = Symbol(`wasm-${type}-${this.stats.allocated}`);
273
+ this.resources.set(token, {
274
+ ref: new WeakRef(resource),
275
+ entry: { type, entityId },
276
+ });
277
+ this.registry.register(resource, { token }, resource);
278
+ this.stats.allocated++;
279
+ return token;
280
+ }
281
+
282
+ release(token: symbol) {
283
+ const entry = this.resources.get(token);
284
+ if (!entry) return false;
285
+ const resource = entry.ref.deref();
286
+ if (resource) {
287
+ try {
288
+ resource.delete();
289
+ this.registry.unregister(resource);
290
+ } catch {}
291
+ }
292
+ this.resources.delete(token);
293
+ this.stats.released++;
294
+ return true;
295
+ }
296
+
297
+ releaseByEntity(entityId: number) {
298
+ let released = 0;
299
+ for (const [token, { entry }] of this.resources) {
300
+ if (entry.entityId === entityId) {
301
+ if (this.release(token)) released++;
302
+ }
303
+ }
304
+ return released;
305
+ }
306
+
307
+ flushCleanupQueue() {
308
+ const count = this.cleanupQueue.length;
309
+ for (const token of this.cleanupQueue) {
310
+ this.release(token);
311
+ }
312
+ this.cleanupQueue = [];
313
+ return count;
314
+ }
315
+
316
+ getStats() {
317
+ return {
318
+ totalAllocated: this.stats.allocated,
319
+ totalReleased: this.stats.released,
320
+ currentActive: this.resources.size,
321
+ leaked: this.stats.allocated - this.stats.released - this.resources.size,
322
+ };
323
+ }
324
+ }
325
+
326
+ (window as any).__VS_RESOURCE_MANAGER__ = new TestResourceManager();
327
+ });
328
+ }
329
+
330
+ async function runHMRStressTest(
331
+ page: Page,
332
+ cycles: number,
333
+ ): Promise<{ allocated: number; released: number; leaked: number }> {
334
+ return await page.evaluate(async (numCycles) => {
335
+ const manager = (window as any).__VS_RESOURCE_MANAGER__;
336
+ const renderer = (window as any).__VS_RENDERER__;
337
+
338
+ for (let i = 0; i < numCycles; i++) {
339
+ // Simulate HMR: add entities
340
+ const tokens: symbol[] = [];
341
+ for (let j = 0; j < 5; j++) {
342
+ const entityId = i * 5 + j;
343
+ const mock = {
344
+ deleted: false,
345
+ delete() { this.deleted = true; },
346
+ isDeleted() { return this.deleted; },
347
+ };
348
+ tokens.push(manager.register(mock, 'paint', entityId));
349
+ }
350
+
351
+ // Simulate HMR: remove entities (explicit release)
352
+ for (const token of tokens) {
353
+ manager.release(token);
354
+ }
355
+
356
+ // Yield to event loop periodically
357
+ if (i % 1000 === 0) {
358
+ await new Promise(r => setTimeout(r, 0));
359
+ }
360
+ }
361
+
362
+ return manager.getStats();
363
+ }, cycles);
364
+ }
365
+
366
+ async function forceGC(cdp: CDPSession): Promise<void> {
367
+ for (let i = 0; i < CONFIG.GC_ATTEMPTS; i++) {
368
+ await cdp.send('HeapProfiler.collectGarbage');
369
+ await new Promise(r => setTimeout(r, 100));
370
+ }
371
+ }
372
+
373
+ async function flushFinalizationRegistry(page: Page): Promise<number> {
374
+ return await page.evaluate(() => {
375
+ return (window as any).__VS_RESOURCE_MANAGER__.flushCleanupQueue();
376
+ });
377
+ }
378
+
379
+ async function measureMemory(page: Page, cdp: CDPSession): Promise<MemoryMetrics> {
380
+ const metrics = await cdp.send('Performance.getMetrics');
381
+ const jsHeapSizeUsed = metrics.metrics.find(m => m.name === 'JSHeapUsedSize')?.value ?? 0;
382
+ const jsHeapSizeTotal = metrics.metrics.find(m => m.name === 'JSHeapTotalSize')?.value ?? 0;
383
+
384
+ return {
385
+ jsHeapSizeUsed,
386
+ jsHeapSizeTotal,
387
+ wasmHeapEstimate: 0, // CDP doesn't directly expose WASM heap
388
+ timestamp: Date.now(),
389
+ };
390
+ }
391
+
392
+ function formatBytes(bytes: number): string {
393
+ if (bytes < 1024) return `${bytes} B`;
394
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
395
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
396
+ }