webgpu-inspector-cli 0.1.0__py3-none-any.whl
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.
- webgpu_inspector_cli/__init__.py +3 -0
- webgpu_inspector_cli/__main__.py +5 -0
- webgpu_inspector_cli/commands/__init__.py +0 -0
- webgpu_inspector_cli/commands/browser.py +106 -0
- webgpu_inspector_cli/commands/capture.py +197 -0
- webgpu_inspector_cli/commands/errors.py +82 -0
- webgpu_inspector_cli/commands/objects.py +129 -0
- webgpu_inspector_cli/commands/shaders.py +108 -0
- webgpu_inspector_cli/commands/status.py +79 -0
- webgpu_inspector_cli/core/__init__.py +0 -0
- webgpu_inspector_cli/core/bridge.py +189 -0
- webgpu_inspector_cli/core/session.py +40 -0
- webgpu_inspector_cli/js/collector.js +439 -0
- webgpu_inspector_cli/skills/SKILL.md +87 -0
- webgpu_inspector_cli/tests/__init__.py +0 -0
- webgpu_inspector_cli/tests/test_core.py +151 -0
- webgpu_inspector_cli/tests/test_full_e2e.py +200 -0
- webgpu_inspector_cli/utils/__init__.py +0 -0
- webgpu_inspector_cli/utils/repl_skin.py +523 -0
- webgpu_inspector_cli/webgpu_inspector_cli.py +91 -0
- webgpu_inspector_cli-0.1.0.dist-info/METADATA +14 -0
- webgpu_inspector_cli-0.1.0.dist-info/RECORD +25 -0
- webgpu_inspector_cli-0.1.0.dist-info/WHEEL +5 -0
- webgpu_inspector_cli-0.1.0.dist-info/entry_points.txt +2 -0
- webgpu_inspector_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* collector.js - Injected into the page to collect WebGPU Inspector messages.
|
|
3
|
+
*
|
|
4
|
+
* Listens for __WebGPUInspector CustomEvents and accumulates GPU object state,
|
|
5
|
+
* validation errors, frame timing, and capture results. Exposes a query API
|
|
6
|
+
* on window.__wgi that the Python bridge calls via page.evaluate().
|
|
7
|
+
*/
|
|
8
|
+
(function () {
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
if (window.__wgi) return; // Already injected
|
|
12
|
+
|
|
13
|
+
// --- Action constants (mirrors src/utils/actions.js) ---
|
|
14
|
+
const Actions = {
|
|
15
|
+
AddObject: "webgpu_inspect_add_object",
|
|
16
|
+
DeleteObject: "webgpu_inspect_delete_object",
|
|
17
|
+
DeleteObjects: "webgpu_inspect_delete_objects",
|
|
18
|
+
ObjectSetLabel: "webgpu_inspect_object_set_label",
|
|
19
|
+
ResolveAsyncObject: "webgpu_inspect_resolve_async_object",
|
|
20
|
+
ValidationError: "webgpu_inspect_validation_error",
|
|
21
|
+
MemoryLeakWarning: "webgpu_inspect_memory_leak_warning",
|
|
22
|
+
DeltaTime: "webgpu_inspect_delta_time",
|
|
23
|
+
CaptureFrameResults: "webgpu_inspect_capture_frame_results",
|
|
24
|
+
CaptureFrameCommands: "webgpu_inspect_capture_frame_commands",
|
|
25
|
+
CaptureTextureData: "webgpu_inspect_capture_texture_data",
|
|
26
|
+
CaptureBufferData: "webgpu_inspect_capture_buffer_data",
|
|
27
|
+
CaptureTextureFrames: "webgpu_inspect_capture_texture_frames",
|
|
28
|
+
Recording: "webgpu_record_recording",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const PanelActions = {
|
|
32
|
+
RequestTexture: "webgpu_inspect_request_texture",
|
|
33
|
+
CompileShader: "webgpu_inspect_compile_shader",
|
|
34
|
+
RevertShader: "webgpu_inspect_revert_shader",
|
|
35
|
+
Capture: "webgpu_inspector_capture",
|
|
36
|
+
InitializeInspector: "webgpu_initialize_inspector",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// --- State ---
|
|
40
|
+
const objects = new Map(); // id -> { id, type, descriptor, label, stacktrace, parent, pending }
|
|
41
|
+
const errors = []; // [{ id, objectId, message, stacktrace, timestamp }]
|
|
42
|
+
const memoryLeaks = [];
|
|
43
|
+
let errorCount = 0;
|
|
44
|
+
let deltaFrameTime = -1;
|
|
45
|
+
let totalTextureMemory = 0;
|
|
46
|
+
let totalBufferMemory = 0;
|
|
47
|
+
|
|
48
|
+
// Capture state
|
|
49
|
+
let captureStatus = "idle"; // "idle" | "pending" | "complete"
|
|
50
|
+
let capturedFrameResults = null;
|
|
51
|
+
let capturedCommands = null;
|
|
52
|
+
let capturedTextures = new Map(); // id -> { chunks: [], totalChunks, assembled }
|
|
53
|
+
let capturedBuffers = new Map(); // id -> { data }
|
|
54
|
+
|
|
55
|
+
// --- Texture memory calculation ---
|
|
56
|
+
const FORMAT_SIZES = {
|
|
57
|
+
"r8unorm": 1, "r8snorm": 1, "r8uint": 1, "r8sint": 1,
|
|
58
|
+
"r16uint": 2, "r16sint": 2, "r16float": 2,
|
|
59
|
+
"rg8unorm": 2, "rg8snorm": 2, "rg8uint": 2, "rg8sint": 2,
|
|
60
|
+
"r32uint": 4, "r32sint": 4, "r32float": 4,
|
|
61
|
+
"rg16uint": 4, "rg16sint": 4, "rg16float": 4,
|
|
62
|
+
"rgba8unorm": 4, "rgba8unorm-srgb": 4, "rgba8snorm": 4,
|
|
63
|
+
"rgba8uint": 4, "rgba8sint": 4,
|
|
64
|
+
"bgra8unorm": 4, "bgra8unorm-srgb": 4,
|
|
65
|
+
"rgb10a2uint": 4, "rgb10a2unorm": 4, "rg11b10ufloat": 4,
|
|
66
|
+
"rg32uint": 8, "rg32sint": 8, "rg32float": 8,
|
|
67
|
+
"rgba16uint": 8, "rgba16sint": 8, "rgba16float": 8,
|
|
68
|
+
"rgba32uint": 16, "rgba32sint": 16, "rgba32float": 16,
|
|
69
|
+
"depth16unorm": 2, "depth24plus": 4, "depth24plus-stencil8": 4,
|
|
70
|
+
"depth32float": 4, "depth32float-stencil8": 5,
|
|
71
|
+
"stencil8": 1,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function getTextureGpuSize(descriptor) {
|
|
75
|
+
if (!descriptor) return 0;
|
|
76
|
+
const format = descriptor.format;
|
|
77
|
+
const bpp = FORMAT_SIZES[format];
|
|
78
|
+
if (bpp === undefined) return 0;
|
|
79
|
+
const w = descriptor.size?.[0] ?? descriptor.size?.width ?? 1;
|
|
80
|
+
const h = descriptor.size?.[1] ?? descriptor.size?.height ?? 1;
|
|
81
|
+
const d = descriptor.size?.[2] ?? descriptor.size?.depthOrArrayLayers ?? 1;
|
|
82
|
+
const mips = descriptor.mipLevelCount ?? 1;
|
|
83
|
+
let total = 0;
|
|
84
|
+
for (let m = 0; m < mips; m++) {
|
|
85
|
+
const mw = Math.max(1, w >> m);
|
|
86
|
+
const mh = Math.max(1, h >> m);
|
|
87
|
+
total += mw * mh * d * bpp;
|
|
88
|
+
}
|
|
89
|
+
return total;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Message handler ---
|
|
93
|
+
function handleMessage(detail) {
|
|
94
|
+
if (!detail || !detail.__webgpuInspector || !detail.__webgpuInspectorPage) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const action = detail.action;
|
|
98
|
+
|
|
99
|
+
switch (action) {
|
|
100
|
+
case Actions.AddObject: {
|
|
101
|
+
let descriptor = null;
|
|
102
|
+
try {
|
|
103
|
+
descriptor = detail.descriptor ? JSON.parse(detail.descriptor) : null;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
descriptor = null;
|
|
106
|
+
}
|
|
107
|
+
const obj = {
|
|
108
|
+
id: detail.id,
|
|
109
|
+
type: detail.type,
|
|
110
|
+
descriptor: descriptor,
|
|
111
|
+
label: descriptor?.label || null,
|
|
112
|
+
stacktrace: detail.stacktrace || "",
|
|
113
|
+
parent: detail.parent ?? null,
|
|
114
|
+
pending: !!detail.pending,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Track memory
|
|
118
|
+
if (detail.type === "Buffer") {
|
|
119
|
+
obj.size = descriptor?.size ?? 0;
|
|
120
|
+
totalBufferMemory += obj.size;
|
|
121
|
+
} else if (detail.type === "Texture") {
|
|
122
|
+
// If texture already exists (reconfigured), update it
|
|
123
|
+
const prev = objects.get(detail.id);
|
|
124
|
+
if (prev && prev.type === "Texture") {
|
|
125
|
+
totalTextureMemory -= prev.gpuSize || 0;
|
|
126
|
+
}
|
|
127
|
+
obj.gpuSize = getTextureGpuSize(descriptor);
|
|
128
|
+
totalTextureMemory += obj.gpuSize;
|
|
129
|
+
} else if (detail.type === "ShaderModule") {
|
|
130
|
+
obj.code = descriptor?.code || null;
|
|
131
|
+
obj.size = descriptor?.code?.length ?? 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
objects.set(detail.id, obj);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case Actions.DeleteObject: {
|
|
139
|
+
const obj = objects.get(detail.id);
|
|
140
|
+
if (obj) {
|
|
141
|
+
if (obj.type === "Buffer") totalBufferMemory -= obj.size || 0;
|
|
142
|
+
if (obj.type === "Texture") totalTextureMemory -= obj.gpuSize || 0;
|
|
143
|
+
objects.delete(detail.id);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case Actions.DeleteObjects: {
|
|
149
|
+
const ids = detail.idList || [];
|
|
150
|
+
for (const id of ids) {
|
|
151
|
+
const obj = objects.get(id);
|
|
152
|
+
if (obj) {
|
|
153
|
+
if (obj.type === "Buffer") totalBufferMemory -= obj.size || 0;
|
|
154
|
+
if (obj.type === "Texture") totalTextureMemory -= obj.gpuSize || 0;
|
|
155
|
+
objects.delete(id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case Actions.ObjectSetLabel: {
|
|
162
|
+
const obj = objects.get(detail.id);
|
|
163
|
+
if (obj) obj.label = detail.label;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case Actions.ResolveAsyncObject: {
|
|
168
|
+
const obj = objects.get(detail.id);
|
|
169
|
+
if (obj) obj.pending = false;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case Actions.ValidationError: {
|
|
174
|
+
errorCount++;
|
|
175
|
+
errors.push({
|
|
176
|
+
id: errorCount,
|
|
177
|
+
objectId: detail.id ?? 0,
|
|
178
|
+
message: detail.message,
|
|
179
|
+
stacktrace: detail.stacktrace || "",
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case Actions.MemoryLeakWarning: {
|
|
186
|
+
memoryLeaks.push({
|
|
187
|
+
id: detail.id,
|
|
188
|
+
type: detail.type,
|
|
189
|
+
message: detail.message,
|
|
190
|
+
timestamp: Date.now(),
|
|
191
|
+
});
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case Actions.DeltaTime: {
|
|
196
|
+
deltaFrameTime = detail.deltaTime;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case Actions.CaptureFrameResults: {
|
|
201
|
+
capturedFrameResults = {
|
|
202
|
+
frame: detail.frame,
|
|
203
|
+
count: detail.count,
|
|
204
|
+
batches: detail.batches,
|
|
205
|
+
};
|
|
206
|
+
captureStatus = "complete";
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case Actions.CaptureFrameCommands: {
|
|
211
|
+
capturedCommands = detail;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case Actions.CaptureTextureData: {
|
|
216
|
+
const texId = detail.id;
|
|
217
|
+
if (!capturedTextures.has(texId)) {
|
|
218
|
+
capturedTextures.set(texId, { chunks: [], totalChunks: 0, complete: false });
|
|
219
|
+
}
|
|
220
|
+
const entry = capturedTextures.get(texId);
|
|
221
|
+
if (detail.index !== undefined) {
|
|
222
|
+
entry.chunks[detail.index] = detail.data;
|
|
223
|
+
entry.totalChunks = detail.count || entry.totalChunks;
|
|
224
|
+
// Check if all chunks received
|
|
225
|
+
const received = entry.chunks.filter(c => c !== undefined).length;
|
|
226
|
+
if (received >= entry.totalChunks && entry.totalChunks > 0) {
|
|
227
|
+
entry.complete = true;
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
// Single chunk
|
|
231
|
+
entry.chunks = [detail.data];
|
|
232
|
+
entry.totalChunks = 1;
|
|
233
|
+
entry.complete = true;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case Actions.CaptureBufferData: {
|
|
239
|
+
const bufId = detail.id;
|
|
240
|
+
capturedBuffers.set(bufId, {
|
|
241
|
+
data: detail.data,
|
|
242
|
+
offset: detail.offset || 0,
|
|
243
|
+
size: detail.size || 0,
|
|
244
|
+
});
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Listen for inspector messages ---
|
|
251
|
+
window.addEventListener("__WebGPUInspector", (event) => {
|
|
252
|
+
handleMessage(event.detail);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// --- Query API ---
|
|
256
|
+
window.__wgi = {
|
|
257
|
+
getObjects(type) {
|
|
258
|
+
const result = [];
|
|
259
|
+
for (const obj of objects.values()) {
|
|
260
|
+
if (!type || obj.type === type) {
|
|
261
|
+
result.push({
|
|
262
|
+
id: obj.id,
|
|
263
|
+
type: obj.type,
|
|
264
|
+
label: obj.label,
|
|
265
|
+
descriptor: obj.descriptor,
|
|
266
|
+
stacktrace: obj.stacktrace,
|
|
267
|
+
parent: obj.parent,
|
|
268
|
+
pending: obj.pending,
|
|
269
|
+
size: obj.size,
|
|
270
|
+
gpuSize: obj.gpuSize,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
getObject(id) {
|
|
278
|
+
const obj = objects.get(id);
|
|
279
|
+
if (!obj) return null;
|
|
280
|
+
return {
|
|
281
|
+
id: obj.id,
|
|
282
|
+
type: obj.type,
|
|
283
|
+
label: obj.label,
|
|
284
|
+
descriptor: obj.descriptor,
|
|
285
|
+
stacktrace: obj.stacktrace,
|
|
286
|
+
parent: obj.parent,
|
|
287
|
+
pending: obj.pending,
|
|
288
|
+
size: obj.size,
|
|
289
|
+
gpuSize: obj.gpuSize,
|
|
290
|
+
code: obj.code,
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
getObjectCount() {
|
|
295
|
+
return objects.size;
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
getErrors() {
|
|
299
|
+
return errors.slice();
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
getErrorCount() {
|
|
303
|
+
return errors.length;
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
clearErrors() {
|
|
307
|
+
errors.length = 0;
|
|
308
|
+
errorCount = 0;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
getFrameRate() {
|
|
312
|
+
if (deltaFrameTime <= 0) return { fps: 0, deltaTime: -1 };
|
|
313
|
+
return {
|
|
314
|
+
fps: Math.round(1000 / deltaFrameTime),
|
|
315
|
+
deltaTime: deltaFrameTime,
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
getMemoryUsage() {
|
|
320
|
+
return {
|
|
321
|
+
totalTextureMemory: totalTextureMemory,
|
|
322
|
+
totalBufferMemory: totalBufferMemory,
|
|
323
|
+
totalMemory: totalTextureMemory + totalBufferMemory,
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
getSummary() {
|
|
328
|
+
const typeCounts = {};
|
|
329
|
+
for (const obj of objects.values()) {
|
|
330
|
+
typeCounts[obj.type] = (typeCounts[obj.type] || 0) + 1;
|
|
331
|
+
}
|
|
332
|
+
const fr = deltaFrameTime > 0 ? Math.round(1000 / deltaFrameTime) : 0;
|
|
333
|
+
return {
|
|
334
|
+
objectCount: objects.size,
|
|
335
|
+
typeCounts: typeCounts,
|
|
336
|
+
errorCount: errors.length,
|
|
337
|
+
fps: fr,
|
|
338
|
+
deltaTime: deltaFrameTime,
|
|
339
|
+
totalTextureMemory: totalTextureMemory,
|
|
340
|
+
totalBufferMemory: totalBufferMemory,
|
|
341
|
+
totalMemory: totalTextureMemory + totalBufferMemory,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
// --- Capture ---
|
|
346
|
+
|
|
347
|
+
requestCapture(options) {
|
|
348
|
+
captureStatus = "pending";
|
|
349
|
+
capturedFrameResults = null;
|
|
350
|
+
capturedCommands = null;
|
|
351
|
+
capturedTextures = new Map();
|
|
352
|
+
capturedBuffers = new Map();
|
|
353
|
+
|
|
354
|
+
const data = options || {};
|
|
355
|
+
window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
|
|
356
|
+
detail: {
|
|
357
|
+
__webgpuInspector: true,
|
|
358
|
+
action: "webgpu_inspector_capture",
|
|
359
|
+
data: JSON.stringify(data),
|
|
360
|
+
}
|
|
361
|
+
}));
|
|
362
|
+
return true;
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
getCaptureStatus() {
|
|
366
|
+
return captureStatus;
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
getCapturedFrameResults() {
|
|
370
|
+
return capturedFrameResults;
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
getCapturedCommands() {
|
|
374
|
+
return capturedCommands;
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
requestTexture(id, mipLevel) {
|
|
378
|
+
window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
|
|
379
|
+
detail: {
|
|
380
|
+
__webgpuInspector: true,
|
|
381
|
+
action: "webgpu_inspect_request_texture",
|
|
382
|
+
id: id,
|
|
383
|
+
mipLevel: mipLevel || 0,
|
|
384
|
+
}
|
|
385
|
+
}));
|
|
386
|
+
return true;
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
getTextureData(id) {
|
|
390
|
+
const entry = capturedTextures.get(id);
|
|
391
|
+
if (!entry) return null;
|
|
392
|
+
return {
|
|
393
|
+
complete: entry.complete,
|
|
394
|
+
totalChunks: entry.totalChunks,
|
|
395
|
+
receivedChunks: entry.chunks.filter(c => c !== undefined).length,
|
|
396
|
+
data: entry.complete ? entry.chunks.join("") : null,
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
getBufferData(id) {
|
|
401
|
+
return capturedBuffers.get(id) || null;
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// --- Shaders ---
|
|
405
|
+
|
|
406
|
+
getShaderCode(id) {
|
|
407
|
+
const obj = objects.get(id);
|
|
408
|
+
if (!obj || obj.type !== "ShaderModule") return null;
|
|
409
|
+
return obj.code || (obj.descriptor?.code ?? null);
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
compileShader(id, code) {
|
|
413
|
+
window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
|
|
414
|
+
detail: {
|
|
415
|
+
__webgpuInspector: true,
|
|
416
|
+
action: "webgpu_inspect_compile_shader",
|
|
417
|
+
id: id,
|
|
418
|
+
code: code,
|
|
419
|
+
}
|
|
420
|
+
}));
|
|
421
|
+
return true;
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
revertShader(id) {
|
|
425
|
+
window.dispatchEvent(new CustomEvent("__WebGPUInspector", {
|
|
426
|
+
detail: {
|
|
427
|
+
__webgpuInspector: true,
|
|
428
|
+
action: "webgpu_inspect_revert_shader",
|
|
429
|
+
id: id,
|
|
430
|
+
}
|
|
431
|
+
}));
|
|
432
|
+
return true;
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
getMemoryLeaks() {
|
|
436
|
+
return memoryLeaks.slice();
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
})();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "webgpu-inspector-cli"
|
|
3
|
+
description: "Debug WebGPU applications from the command line - inspect GPU objects, view shaders, capture frames, check validation errors"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# webgpu-inspector-cli
|
|
7
|
+
|
|
8
|
+
CLI tool for debugging WebGPU applications. Launches a browser, injects the WebGPU Inspector, and provides commands to inspect GPU state.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
git clone --recurse-submodules https://github.com/scasekar/webgpu-inspector-cli
|
|
14
|
+
cd webgpu-inspector-cli/agent-harness && pip install -e .
|
|
15
|
+
python -m playwright install chromium
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Launch browser with your WebGPU app
|
|
22
|
+
webgpu-inspector-cli browser launch --url https://your-app.com
|
|
23
|
+
|
|
24
|
+
# 2. Check for problems
|
|
25
|
+
webgpu-inspector-cli --json errors list
|
|
26
|
+
webgpu-inspector-cli --json status summary
|
|
27
|
+
|
|
28
|
+
# 3. Inspect GPU objects
|
|
29
|
+
webgpu-inspector-cli --json objects list
|
|
30
|
+
webgpu-inspector-cli --json objects inspect --id 8
|
|
31
|
+
|
|
32
|
+
# 4. View shader code
|
|
33
|
+
webgpu-inspector-cli shaders view --id 8
|
|
34
|
+
|
|
35
|
+
# 5. Capture a frame
|
|
36
|
+
webgpu-inspector-cli --json capture frame
|
|
37
|
+
|
|
38
|
+
# 6. Clean up
|
|
39
|
+
webgpu-inspector-cli browser close
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Command Groups
|
|
43
|
+
|
|
44
|
+
### browser - Session lifecycle
|
|
45
|
+
- `browser launch --url URL [--headless] [--gpu-backend BACKEND]`
|
|
46
|
+
- `browser close`
|
|
47
|
+
- `browser navigate --url URL`
|
|
48
|
+
- `browser screenshot -o PATH`
|
|
49
|
+
- `browser status`
|
|
50
|
+
|
|
51
|
+
### objects - GPU object inspection
|
|
52
|
+
- `objects list [--type TYPE]` - List all GPU objects (Adapter, Device, Buffer, Texture, ShaderModule, RenderPipeline, etc.)
|
|
53
|
+
- `objects inspect --id ID` - Full details including descriptor and creation stacktrace
|
|
54
|
+
- `objects search --label PATTERN` - Find objects by label
|
|
55
|
+
- `objects memory` - GPU memory breakdown
|
|
56
|
+
|
|
57
|
+
### capture - Frame capture
|
|
58
|
+
- `capture frame [--timeout N]` - Capture next frame's GPU commands
|
|
59
|
+
- `capture commands [--pass-index N]` - List commands from captured frame
|
|
60
|
+
- `capture texture --id ID [-o PATH]` - Read texture pixels, optionally save as PNG
|
|
61
|
+
- `capture buffer --id ID [--format hex|float32|uint32]` - Read buffer data
|
|
62
|
+
|
|
63
|
+
### shaders - Shader inspection
|
|
64
|
+
- `shaders list` - List all shader modules
|
|
65
|
+
- `shaders view --id ID` - View WGSL source code
|
|
66
|
+
- `shaders compile --id ID --file PATH` - Hot-replace shader code
|
|
67
|
+
- `shaders revert --id ID` - Revert to original
|
|
68
|
+
|
|
69
|
+
### errors - Validation errors
|
|
70
|
+
- `errors list` - All validation errors with stacktraces
|
|
71
|
+
- `errors watch [--timeout N]` - Stream errors in real-time
|
|
72
|
+
- `errors clear` - Reset error history
|
|
73
|
+
|
|
74
|
+
### status - Runtime monitoring
|
|
75
|
+
- `status summary` - Object counts, memory, FPS, error count
|
|
76
|
+
- `status fps` - Current frame rate
|
|
77
|
+
- `status memory` - Memory breakdown
|
|
78
|
+
|
|
79
|
+
## Agent Guidance
|
|
80
|
+
|
|
81
|
+
- Always use `--json` flag for machine-readable output
|
|
82
|
+
- Launch browser first with `browser launch --url URL`
|
|
83
|
+
- Close browser when done with `browser close`
|
|
84
|
+
- For debugging workflow: check `errors list` first, then `objects list` to understand GPU state
|
|
85
|
+
- Shader IDs are integers - get them from `shaders list` before using `shaders view`
|
|
86
|
+
- Frame capture is async - `capture frame` polls until complete (default 30s timeout)
|
|
87
|
+
- Texture data can be saved as PNG with `capture texture --id ID -o output.png`
|
|
File without changes
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Unit tests for WebGPU Inspector CLI core modules."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import pytest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# --- Session Tests ---
|
|
11
|
+
|
|
12
|
+
class TestSession:
|
|
13
|
+
def setup_method(self):
|
|
14
|
+
from webgpu_inspector_cli.core.session import Session
|
|
15
|
+
self.session = Session()
|
|
16
|
+
|
|
17
|
+
def test_push_pop_shader_edit(self):
|
|
18
|
+
self.session.push_shader_edit(1, "original code")
|
|
19
|
+
result = self.session.pop_shader_edit(1)
|
|
20
|
+
assert result == "original code"
|
|
21
|
+
|
|
22
|
+
def test_pop_empty_returns_none(self):
|
|
23
|
+
result = self.session.pop_shader_edit(999)
|
|
24
|
+
assert result is None
|
|
25
|
+
|
|
26
|
+
def test_clear_shader_edits(self):
|
|
27
|
+
self.session.push_shader_edit(1, "v1")
|
|
28
|
+
self.session.push_shader_edit(1, "v2")
|
|
29
|
+
self.session.clear_shader_edits(1)
|
|
30
|
+
assert not self.session.has_shader_edits(1)
|
|
31
|
+
|
|
32
|
+
def test_has_shader_edits(self):
|
|
33
|
+
assert not self.session.has_shader_edits(1)
|
|
34
|
+
self.session.push_shader_edit(1, "code")
|
|
35
|
+
assert self.session.has_shader_edits(1)
|
|
36
|
+
|
|
37
|
+
def test_multiple_edits_stack(self):
|
|
38
|
+
"""Edits should pop in LIFO order."""
|
|
39
|
+
self.session.push_shader_edit(1, "v1")
|
|
40
|
+
self.session.push_shader_edit(1, "v2")
|
|
41
|
+
self.session.push_shader_edit(1, "v3")
|
|
42
|
+
assert self.session.pop_shader_edit(1) == "v3"
|
|
43
|
+
assert self.session.pop_shader_edit(1) == "v2"
|
|
44
|
+
assert self.session.pop_shader_edit(1) == "v1"
|
|
45
|
+
assert self.session.pop_shader_edit(1) is None
|
|
46
|
+
|
|
47
|
+
def test_separate_shader_histories(self):
|
|
48
|
+
"""Different shader IDs should have independent histories."""
|
|
49
|
+
self.session.push_shader_edit(1, "shader1_v1")
|
|
50
|
+
self.session.push_shader_edit(2, "shader2_v1")
|
|
51
|
+
assert self.session.pop_shader_edit(1) == "shader1_v1"
|
|
52
|
+
assert self.session.pop_shader_edit(2) == "shader2_v1"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --- Bridge Path Resolution Tests ---
|
|
56
|
+
|
|
57
|
+
class TestBridgePaths:
|
|
58
|
+
def test_find_inspector_js(self):
|
|
59
|
+
from webgpu_inspector_cli.core.bridge import _find_inspector_js
|
|
60
|
+
path = _find_inspector_js()
|
|
61
|
+
assert path.exists()
|
|
62
|
+
assert path.name == "webgpu_inspector_loader.js"
|
|
63
|
+
|
|
64
|
+
def test_find_collector_js(self):
|
|
65
|
+
from webgpu_inspector_cli.core.bridge import _find_collector_js
|
|
66
|
+
path = _find_collector_js()
|
|
67
|
+
assert path.exists()
|
|
68
|
+
assert path.name == "collector.js"
|
|
69
|
+
|
|
70
|
+
def test_bridge_not_connected(self):
|
|
71
|
+
from webgpu_inspector_cli.core.bridge import Bridge
|
|
72
|
+
bridge = Bridge()
|
|
73
|
+
assert not bridge.is_connected
|
|
74
|
+
with pytest.raises(RuntimeError, match="No browser session"):
|
|
75
|
+
bridge.query("getSummary")
|
|
76
|
+
|
|
77
|
+
def test_bridge_require_not_connected(self):
|
|
78
|
+
from webgpu_inspector_cli.core.bridge import require_bridge, Bridge
|
|
79
|
+
import webgpu_inspector_cli.core.bridge as bridge_mod
|
|
80
|
+
# Reset global singleton
|
|
81
|
+
bridge_mod._bridge = Bridge()
|
|
82
|
+
with pytest.raises(RuntimeError, match="No active browser session"):
|
|
83
|
+
require_bridge()
|
|
84
|
+
bridge_mod._bridge = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- Format Helpers Tests ---
|
|
88
|
+
|
|
89
|
+
class TestFormatHelpers:
|
|
90
|
+
def test_format_bytes_zero(self):
|
|
91
|
+
from webgpu_inspector_cli.commands.objects import _format_bytes
|
|
92
|
+
assert _format_bytes(0) == "0 B"
|
|
93
|
+
|
|
94
|
+
def test_format_bytes_small(self):
|
|
95
|
+
from webgpu_inspector_cli.commands.objects import _format_bytes
|
|
96
|
+
assert _format_bytes(512) == "512 B"
|
|
97
|
+
|
|
98
|
+
def test_format_bytes_kb(self):
|
|
99
|
+
from webgpu_inspector_cli.commands.objects import _format_bytes
|
|
100
|
+
result = _format_bytes(2048)
|
|
101
|
+
assert "KB" in result
|
|
102
|
+
|
|
103
|
+
def test_format_bytes_mb(self):
|
|
104
|
+
from webgpu_inspector_cli.commands.objects import _format_bytes
|
|
105
|
+
result = _format_bytes(4608000)
|
|
106
|
+
assert "MB" in result
|
|
107
|
+
|
|
108
|
+
def test_format_bytes_none(self):
|
|
109
|
+
from webgpu_inspector_cli.commands.objects import _format_bytes
|
|
110
|
+
assert _format_bytes(None) == "0 B"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --- Collector JS Content Tests ---
|
|
114
|
+
|
|
115
|
+
class TestCollectorJS:
|
|
116
|
+
def test_collector_js_exists_and_valid(self):
|
|
117
|
+
from webgpu_inspector_cli.core.bridge import _find_collector_js
|
|
118
|
+
path = _find_collector_js()
|
|
119
|
+
content = path.read_text()
|
|
120
|
+
# Check it defines window.__wgi
|
|
121
|
+
assert "window.__wgi" in content
|
|
122
|
+
# Check it handles key actions
|
|
123
|
+
assert "AddObject" in content
|
|
124
|
+
assert "DeleteObject" in content
|
|
125
|
+
assert "ValidationError" in content
|
|
126
|
+
assert "DeltaTime" in content
|
|
127
|
+
assert "CaptureFrameResults" in content
|
|
128
|
+
|
|
129
|
+
def test_collector_has_query_api(self):
|
|
130
|
+
from webgpu_inspector_cli.core.bridge import _find_collector_js
|
|
131
|
+
content = _find_collector_js().read_text()
|
|
132
|
+
# Check all query functions exist
|
|
133
|
+
for fn in ["getObjects", "getObject", "getErrors", "getFrameRate",
|
|
134
|
+
"getMemoryUsage", "getSummary", "requestCapture",
|
|
135
|
+
"getCaptureStatus", "getShaderCode", "compileShader",
|
|
136
|
+
"revertShader"]:
|
|
137
|
+
assert fn in content, f"Missing query function: {fn}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- CLI Entry Point Tests ---
|
|
141
|
+
|
|
142
|
+
class TestCLIStructure:
|
|
143
|
+
def test_cli_import(self):
|
|
144
|
+
from webgpu_inspector_cli.webgpu_inspector_cli import cli
|
|
145
|
+
assert cli is not None
|
|
146
|
+
|
|
147
|
+
def test_cli_has_commands(self):
|
|
148
|
+
from webgpu_inspector_cli.webgpu_inspector_cli import cli
|
|
149
|
+
command_names = set(cli.commands.keys())
|
|
150
|
+
expected = {"browser", "objects", "capture", "shaders", "errors", "status", "repl"}
|
|
151
|
+
assert expected.issubset(command_names)
|