@clypra/runtime 1.0.0

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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @clypra/runtime — Resource Manager
3
+ *
4
+ * Manages allocation and deallocation of GPU resources.
5
+ */
6
+
7
+ import type { ResourceDescriptor, ResourceStats, ResourceHandle } from "./types";
8
+ import { LRUCache } from "./cache";
9
+
10
+ /**
11
+ * Resource Manager
12
+ *
13
+ * Manages GPU resource lifecycle with LRU caching.
14
+ */
15
+ export class ResourceManager<T = any> {
16
+ private resources = new Map<string, ResourceHandle<T>>();
17
+ private cache: LRUCache<T>;
18
+ private allocator: (descriptor: ResourceDescriptor) => T;
19
+ private deallocator: (resource: T) => void;
20
+
21
+ constructor(allocator: (descriptor: ResourceDescriptor) => T, deallocator: (resource: T) => void, cacheSize: number = 20) {
22
+ this.allocator = allocator;
23
+ this.deallocator = deallocator;
24
+ this.cache = new LRUCache<T>(cacheSize);
25
+ }
26
+
27
+ /**
28
+ * Allocate a resource
29
+ */
30
+ allocate(descriptor: ResourceDescriptor): T {
31
+ // Check if already allocated
32
+ if (this.resources.has(descriptor.id)) {
33
+ const handle = this.resources.get(descriptor.id)!;
34
+ handle.refCount++;
35
+ handle.lastUsed = Date.now();
36
+ return handle.resource;
37
+ }
38
+
39
+ // Try to get from cache
40
+ const cacheKey = this.descriptorKey(descriptor);
41
+ let resource = this.cache.get(cacheKey);
42
+
43
+ if (!resource) {
44
+ // Allocate new resource
45
+ resource = this.allocator(descriptor);
46
+ }
47
+
48
+ // Create handle
49
+ const handle: ResourceHandle<T> = {
50
+ id: descriptor.id,
51
+ resource,
52
+ descriptor,
53
+ refCount: 1,
54
+ lastUsed: Date.now(),
55
+ };
56
+
57
+ this.resources.set(descriptor.id, handle);
58
+ return resource;
59
+ }
60
+
61
+ /**
62
+ * Release a resource
63
+ */
64
+ release(id: string): void {
65
+ const handle = this.resources.get(id);
66
+ if (!handle) return;
67
+
68
+ handle.refCount--;
69
+
70
+ if (handle.refCount <= 0) {
71
+ // Remove from active resources
72
+ this.resources.delete(id);
73
+
74
+ // Add to cache if transient
75
+ if (handle.descriptor.transient) {
76
+ const cacheKey = this.descriptorKey(handle.descriptor);
77
+ const size = this.calculateSize(handle.descriptor);
78
+ this.cache.put(cacheKey, handle.resource, size);
79
+ } else {
80
+ // Deallocate non-transient resources
81
+ this.deallocator(handle.resource);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get a resource
88
+ */
89
+ get(id: string): T | undefined {
90
+ const handle = this.resources.get(id);
91
+ if (handle) {
92
+ handle.lastUsed = Date.now();
93
+ return handle.resource;
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ /**
99
+ * Check if resource exists
100
+ */
101
+ has(id: string): boolean {
102
+ return this.resources.has(id);
103
+ }
104
+
105
+ /**
106
+ * Generate cache key from descriptor
107
+ */
108
+ private descriptorKey(descriptor: ResourceDescriptor): string {
109
+ return `${descriptor.type}:${descriptor.width}x${descriptor.height}:${descriptor.format}`;
110
+ }
111
+
112
+ /**
113
+ * Calculate resource size (for cache management)
114
+ */
115
+ private calculateSize(descriptor: ResourceDescriptor): number {
116
+ const bytesPerPixel = this.getBytesPerPixel(descriptor.format);
117
+ return descriptor.width * descriptor.height * bytesPerPixel;
118
+ }
119
+
120
+ /**
121
+ * Get bytes per pixel for format
122
+ */
123
+ private getBytesPerPixel(format: string): number {
124
+ switch (format) {
125
+ case "rgba8":
126
+ return 4;
127
+ case "rgba16f":
128
+ return 8;
129
+ case "rgba32f":
130
+ return 16;
131
+ case "r8":
132
+ return 1;
133
+ case "depth24":
134
+ return 3;
135
+ default:
136
+ return 4;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get resource statistics
142
+ */
143
+ getStats(): ResourceStats {
144
+ const cacheStats = this.cache.getStats();
145
+ let totalMemory = 0;
146
+
147
+ for (const handle of this.resources.values()) {
148
+ totalMemory += this.calculateSize(handle.descriptor);
149
+ }
150
+
151
+ return {
152
+ allocated: this.resources.size,
153
+ available: cacheStats.capacity - cacheStats.size,
154
+ totalMemory,
155
+ cacheHits: cacheStats.hits,
156
+ cacheMisses: cacheStats.misses,
157
+ evictions: cacheStats.evictions,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Clear all resources
163
+ */
164
+ clear(): void {
165
+ // Deallocate all active resources
166
+ for (const handle of this.resources.values()) {
167
+ this.deallocator(handle.resource);
168
+ }
169
+ this.resources.clear();
170
+
171
+ // Deallocate cached resources
172
+ for (const resource of this.cache.values()) {
173
+ this.deallocator(resource);
174
+ }
175
+ this.cache.clear();
176
+ }
177
+
178
+ /**
179
+ * Dispose the manager
180
+ */
181
+ dispose(): void {
182
+ this.clear();
183
+ }
184
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @clypra/runtime — Resource Types
3
+ */
4
+
5
+ export interface ResourceDescriptor {
6
+ id: string;
7
+ type: "texture" | "buffer";
8
+ width: number;
9
+ height: number;
10
+ format: "rgba8" | "rgba16f" | "rgba32f" | "r8" | "depth24";
11
+ transient: boolean;
12
+ }
13
+
14
+ export interface ResourceStats {
15
+ allocated: number;
16
+ available: number;
17
+ totalMemory: number;
18
+ cacheHits: number;
19
+ cacheMisses: number;
20
+ evictions: number;
21
+ }
22
+
23
+ export interface ResourceHandle<T = any> {
24
+ id: string;
25
+ resource: T;
26
+ descriptor: ResourceDescriptor;
27
+ refCount: number;
28
+ lastUsed: number;
29
+ }
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Benchmark Runner
3
+ *
4
+ * Measures GPU/CPU performance of effects to ensure they meet performance targets.
5
+ * Tracks timing, FPS, memory usage, and generates performance reports.
6
+ *
7
+ * Phase 6 Week 10 - Publishing Pipeline #3
8
+ */
9
+
10
+ export interface BenchmarkConfig {
11
+ effectId: string;
12
+ parameters: Record<string, any>;
13
+ resolution: { width: number; height: number };
14
+ frameCount: number;
15
+ warmupFrames: number;
16
+ targetFPS: number;
17
+ }
18
+
19
+ export interface BenchmarkResult {
20
+ effectId: string;
21
+ resolution: string;
22
+ frameCount: number;
23
+ performance: {
24
+ averageGPUTime: number; // milliseconds
25
+ minGPUTime: number;
26
+ maxGPUTime: number;
27
+ averageCPUTime: number;
28
+ minCPUTime: number;
29
+ maxCPUTime: number;
30
+ averageFPS: number;
31
+ minFPS: number;
32
+ maxFPS: number;
33
+ targetFPS: number;
34
+ meetsTarget: boolean;
35
+ };
36
+ memory: {
37
+ peakUsage: number; // bytes
38
+ averageUsage: number;
39
+ allocations: number;
40
+ };
41
+ passTimings: PassTiming[];
42
+ executedAt: string;
43
+ duration: number; // Total benchmark duration in ms
44
+ }
45
+
46
+ export interface PassTiming {
47
+ passId: string;
48
+ passName: string;
49
+ averageTime: number;
50
+ minTime: number;
51
+ maxTime: number;
52
+ percentage: number; // Percentage of total render time
53
+ }
54
+
55
+ export interface FrameTiming {
56
+ frameNumber: number;
57
+ gpuTime: number;
58
+ cpuTime: number;
59
+ totalTime: number;
60
+ fps: number;
61
+ memoryUsage: number;
62
+ }
63
+
64
+ /**
65
+ * Performance Timer
66
+ */
67
+ export class PerformanceTimer {
68
+ private startTime: number = 0;
69
+ private endTime: number = 0;
70
+
71
+ start(): void {
72
+ this.startTime = performance.now();
73
+ }
74
+
75
+ end(): number {
76
+ this.endTime = performance.now();
77
+ return this.endTime - this.startTime;
78
+ }
79
+
80
+ get elapsed(): number {
81
+ return this.endTime - this.startTime;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * GPU Timer (using WebGL queries)
87
+ */
88
+ export class GPUTimer {
89
+ private gl: WebGLRenderingContext | WebGL2RenderingContext;
90
+ private ext: any;
91
+ private query: any;
92
+
93
+ constructor(gl: WebGLRenderingContext | WebGL2RenderingContext) {
94
+ this.gl = gl;
95
+
96
+ // Try to get timer query extension
97
+ if (gl instanceof WebGL2RenderingContext) {
98
+ // WebGL2 has built-in support
99
+ this.ext = gl;
100
+ } else {
101
+ // WebGL1 needs extension
102
+ this.ext = gl.getExtension("EXT_disjoint_timer_query_webgl2") || gl.getExtension("EXT_disjoint_timer_query");
103
+ }
104
+
105
+ if (!this.ext) {
106
+ console.warn("GPU timing not available (timer query extension not supported)");
107
+ }
108
+ }
109
+
110
+ isAvailable(): boolean {
111
+ return this.ext !== null;
112
+ }
113
+
114
+ beginQuery(): void {
115
+ if (!this.ext) return;
116
+
117
+ if (this.gl instanceof WebGL2RenderingContext) {
118
+ this.query = this.gl.createQuery();
119
+ (this.gl as any).beginQuery((this.gl as any).TIME_ELAPSED_EXT, this.query);
120
+ } else {
121
+ this.query = (this.ext as any).createQueryEXT();
122
+ (this.ext as any).beginQueryEXT((this.ext as any).TIME_ELAPSED_EXT, this.query);
123
+ }
124
+ }
125
+
126
+ endQuery(): void {
127
+ if (!this.ext) return;
128
+
129
+ if (this.gl instanceof WebGL2RenderingContext) {
130
+ (this.gl as any).endQuery((this.gl as any).TIME_ELAPSED_EXT);
131
+ } else {
132
+ (this.ext as any).endQueryEXT((this.ext as any).TIME_ELAPSED_EXT);
133
+ }
134
+ }
135
+
136
+ async getResult(): Promise<number> {
137
+ if (!this.ext || !this.query) return 0;
138
+
139
+ // Wait for result to be available
140
+ await this.waitForResult();
141
+
142
+ let result: number;
143
+
144
+ if (this.gl instanceof WebGL2RenderingContext) {
145
+ result = (this.gl as any).getQueryParameter(this.query, (this.gl as any).QUERY_RESULT);
146
+ } else {
147
+ result = (this.ext as any).getQueryObjectEXT(this.query, (this.ext as any).QUERY_RESULT_EXT);
148
+ }
149
+
150
+ // Convert nanoseconds to milliseconds
151
+ return result / 1000000;
152
+ }
153
+
154
+ private async waitForResult(): Promise<void> {
155
+ return new Promise((resolve) => {
156
+ const checkResult = () => {
157
+ let available: boolean;
158
+
159
+ if (this.gl instanceof WebGL2RenderingContext) {
160
+ available = (this.gl as any).getQueryParameter(this.query, (this.gl as any).QUERY_RESULT_AVAILABLE);
161
+ } else {
162
+ available = (this.ext as any).getQueryObjectEXT(this.query, (this.ext as any).QUERY_RESULT_AVAILABLE_EXT);
163
+ }
164
+
165
+ if (available) {
166
+ resolve();
167
+ } else {
168
+ requestAnimationFrame(checkResult);
169
+ }
170
+ };
171
+
172
+ requestAnimationFrame(checkResult);
173
+ });
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Memory Monitor
179
+ */
180
+ export class MemoryMonitor {
181
+ getUsage(): number {
182
+ // Try to get memory info if available
183
+ if ((performance as any).memory) {
184
+ return (performance as any).memory.usedJSHeapSize;
185
+ }
186
+
187
+ // Fallback: not available in all browsers
188
+ return 0;
189
+ }
190
+
191
+ getPeakUsage(): number {
192
+ if ((performance as any).memory) {
193
+ return (performance as any).memory.totalJSHeapSize;
194
+ }
195
+ return 0;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Benchmark Runner
201
+ */
202
+ export class BenchmarkRunner {
203
+ private memoryMonitor = new MemoryMonitor();
204
+
205
+ /**
206
+ * Run a performance benchmark
207
+ */
208
+ async runBenchmark(config: BenchmarkConfig, renderer: BenchmarkRenderer): Promise<BenchmarkResult> {
209
+ const timings: FrameTiming[] = [];
210
+ const startTime = performance.now();
211
+
212
+ // Warmup phase
213
+ for (let i = 0; i < config.warmupFrames; i++) {
214
+ await renderer.renderFrame(config.effectId, config.parameters, i);
215
+ }
216
+
217
+ // Benchmark phase
218
+ for (let frame = 0; frame < config.frameCount; frame++) {
219
+ const cpuTimer = new PerformanceTimer();
220
+ const memoryBefore = this.memoryMonitor.getUsage();
221
+
222
+ cpuTimer.start();
223
+
224
+ // Render frame (GPU timing handled by renderer)
225
+ const frameResult = await renderer.renderFrame(config.effectId, config.parameters, frame);
226
+
227
+ const cpuTime = cpuTimer.end();
228
+ const memoryAfter = this.memoryMonitor.getUsage();
229
+
230
+ timings.push({
231
+ frameNumber: frame,
232
+ gpuTime: frameResult.gpuTime,
233
+ cpuTime,
234
+ totalTime: frameResult.gpuTime + cpuTime,
235
+ fps: 1000 / (frameResult.gpuTime + cpuTime),
236
+ memoryUsage: memoryAfter - memoryBefore,
237
+ });
238
+ }
239
+
240
+ const totalDuration = performance.now() - startTime;
241
+
242
+ // Calculate statistics
243
+ const gpuTimes = timings.map((t) => t.gpuTime);
244
+ const cpuTimes = timings.map((t) => t.cpuTime);
245
+ const fpsList = timings.map((t) => t.fps);
246
+ const memoryUsages = timings.map((t) => t.memoryUsage);
247
+
248
+ const averageGPUTime = this.average(gpuTimes);
249
+ const minGPUTime = Math.min(...gpuTimes);
250
+ const maxGPUTime = Math.max(...gpuTimes);
251
+
252
+ const averageCPUTime = this.average(cpuTimes);
253
+ const minCPUTime = Math.min(...cpuTimes);
254
+ const maxCPUTime = Math.max(...cpuTimes);
255
+
256
+ const averageFPS = this.average(fpsList);
257
+ const minFPS = Math.min(...fpsList);
258
+ const maxFPS = Math.max(...fpsList);
259
+
260
+ const peakMemory = Math.max(...memoryUsages);
261
+ const averageMemory = this.average(memoryUsages);
262
+
263
+ // Get pass timings from renderer
264
+ const passTimings = await renderer.getPassTimings(config.effectId);
265
+
266
+ return {
267
+ effectId: config.effectId,
268
+ resolution: `${config.resolution.width}x${config.resolution.height}`,
269
+ frameCount: config.frameCount,
270
+ performance: {
271
+ averageGPUTime,
272
+ minGPUTime,
273
+ maxGPUTime,
274
+ averageCPUTime,
275
+ minCPUTime,
276
+ maxCPUTime,
277
+ averageFPS,
278
+ minFPS,
279
+ maxFPS,
280
+ targetFPS: config.targetFPS,
281
+ meetsTarget: averageFPS >= config.targetFPS,
282
+ },
283
+ memory: {
284
+ peakUsage: peakMemory,
285
+ averageUsage: averageMemory,
286
+ allocations: timings.length,
287
+ },
288
+ passTimings,
289
+ executedAt: new Date().toISOString(),
290
+ duration: totalDuration,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Generate a benchmark report
296
+ */
297
+ generateReport(result: BenchmarkResult): string {
298
+ const { performance: perf, memory, passTimings } = result;
299
+
300
+ let report = "# Performance Benchmark Report\n\n";
301
+ report += `**Effect:** ${result.effectId}\n`;
302
+ report += `**Resolution:** ${result.resolution}\n`;
303
+ report += `**Frames Tested:** ${result.frameCount}\n`;
304
+ report += `**Duration:** ${result.duration.toFixed(0)}ms\n`;
305
+ report += `**Date:** ${result.executedAt}\n\n`;
306
+
307
+ // Performance summary
308
+ report += `## Performance ${perf.meetsTarget ? "✅" : "❌"}\n\n`;
309
+ report += `| Metric | Value | Target |\n`;
310
+ report += `|--------|-------|--------|\n`;
311
+ report += `| Average FPS | ${perf.averageFPS.toFixed(1)} | ${perf.targetFPS} |\n`;
312
+ report += `| Min FPS | ${perf.minFPS.toFixed(1)} | ${perf.targetFPS} |\n`;
313
+ report += `| Max FPS | ${perf.maxFPS.toFixed(1)} | - |\n\n`;
314
+
315
+ // Timing breakdown
316
+ report += `## Timing Breakdown\n\n`;
317
+ report += `### GPU Time\n\n`;
318
+ report += `- **Average:** ${perf.averageGPUTime.toFixed(2)}ms\n`;
319
+ report += `- **Min:** ${perf.minGPUTime.toFixed(2)}ms\n`;
320
+ report += `- **Max:** ${perf.maxGPUTime.toFixed(2)}ms\n`;
321
+ report += `- **Target:** <16.67ms (60fps)\n\n`;
322
+
323
+ if (perf.averageGPUTime > 16.67) {
324
+ report += `⚠️ **Warning:** Average GPU time exceeds 60fps budget\n\n`;
325
+ }
326
+
327
+ report += `### CPU Time\n\n`;
328
+ report += `- **Average:** ${perf.averageCPUTime.toFixed(2)}ms\n`;
329
+ report += `- **Min:** ${perf.minCPUTime.toFixed(2)}ms\n`;
330
+ report += `- **Max:** ${perf.maxCPUTime.toFixed(2)}ms\n\n`;
331
+
332
+ // Pass timings
333
+ if (passTimings.length > 0) {
334
+ report += `## Pass Timings\n\n`;
335
+ report += `| Pass | Time | Percentage |\n`;
336
+ report += `|------|------|------------|\n`;
337
+
338
+ for (const pass of passTimings) {
339
+ report += `| ${pass.passName} | ${pass.averageTime.toFixed(2)}ms | ${pass.percentage.toFixed(1)}% |\n`;
340
+ }
341
+ report += `\n`;
342
+ }
343
+
344
+ // Memory usage
345
+ report += `## Memory Usage\n\n`;
346
+ report += `- **Peak:** ${this.formatBytes(memory.peakUsage)}\n`;
347
+ report += `- **Average:** ${this.formatBytes(memory.averageUsage)}\n`;
348
+ report += `- **Allocations:** ${memory.allocations}\n\n`;
349
+
350
+ // Recommendations
351
+ report += `## Recommendations\n\n`;
352
+
353
+ if (!perf.meetsTarget) {
354
+ report += `- ❌ Effect does not meet ${perf.targetFPS} FPS target\n`;
355
+ report += `- Consider optimizing shaders or reducing pass count\n\n`;
356
+ } else {
357
+ report += `- ✅ Effect meets performance target\n\n`;
358
+ }
359
+
360
+ if (perf.averageGPUTime > 10) {
361
+ report += `- ⚠️ GPU time is high (${perf.averageGPUTime.toFixed(2)}ms)\n`;
362
+ report += `- Review pass timings to identify bottlenecks\n\n`;
363
+ }
364
+
365
+ if (passTimings.length > 5) {
366
+ report += `- ⚠️ Effect has ${passTimings.length} render passes\n`;
367
+ report += `- Consider combining passes or using half-resolution buffers\n\n`;
368
+ }
369
+
370
+ return report;
371
+ }
372
+
373
+ private average(numbers: number[]): number {
374
+ return numbers.reduce((a, b) => a + b, 0) / numbers.length;
375
+ }
376
+
377
+ private formatBytes(bytes: number): string {
378
+ if (bytes === 0) return "N/A";
379
+ if (bytes < 1024) return `${bytes}B`;
380
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)}KB`;
381
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Benchmark Renderer Interface
387
+ */
388
+ export interface BenchmarkRenderer {
389
+ renderFrame(effectId: string, parameters: Record<string, any>, frameNumber: number): Promise<{ gpuTime: number }>;
390
+ getPassTimings(effectId: string): Promise<PassTiming[]>;
391
+ }
392
+
393
+ /**
394
+ * Convenience function to run benchmarks
395
+ */
396
+ export async function benchmarkEffect(config: BenchmarkConfig, renderer: BenchmarkRenderer): Promise<BenchmarkResult> {
397
+ const runner = new BenchmarkRunner();
398
+ return await runner.runBenchmark(config, renderer);
399
+ }