@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,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
|
+
}
|