@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.
- package/.releaserc.json +16 -0
- package/package.json +47 -0
- package/src/__tests__/integration.test.ts +345 -0
- package/src/graph/__tests__/builder.test.ts +204 -0
- package/src/graph/__tests__/validator.test.ts +381 -0
- package/src/graph/builder.ts +263 -0
- package/src/graph/index.ts +14 -0
- package/src/graph/types.ts +176 -0
- package/src/graph/validator.ts +208 -0
- package/src/index.ts +28 -0
- package/src/pixi/filters.ts +98 -0
- package/src/pixi/index.ts +11 -0
- package/src/pixi/renderer.ts +375 -0
- package/src/pixi/texture-pool.ts +159 -0
- package/src/pixi/types.ts +58 -0
- package/src/planner/index.ts +10 -0
- package/src/planner/optimizer.ts +247 -0
- package/src/planner/planner.ts +201 -0
- package/src/planner/types.ts +56 -0
- package/src/resources/cache.ts +166 -0
- package/src/resources/index.ts +9 -0
- package/src/resources/manager.ts +184 -0
- package/src/resources/types.ts +29 -0
- package/src/testing/benchmarkRunner.ts +399 -0
- package/src/testing/goldenTests.ts +390 -0
- package/src/validation/effectValidator.ts +571 -0
- package/src/validation/index.ts +9 -0
- package/src/validation/resource-validator.ts +173 -0
- package/src/validation/shader-validator.ts +154 -0
- package/src/validation/types.ts +31 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +18 -0
|
@@ -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
|
+
}
|