@princeofscale/bloxforge 2.20.2

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 (49) hide show
  1. package/assets/Baseplate.rbxl +0 -0
  2. package/dist/index.js +19391 -0
  3. package/package.json +62 -0
  4. package/studio-plugin/INSTALLATION.md +161 -0
  5. package/studio-plugin/MCPInspectorPlugin.rbxmx +171699 -0
  6. package/studio-plugin/MCPPlugin.rbxmx +171699 -0
  7. package/studio-plugin/default.project.json +19 -0
  8. package/studio-plugin/dev.project.json +23 -0
  9. package/studio-plugin/include/LibMP.lua +156378 -0
  10. package/studio-plugin/inspector-icon.png +0 -0
  11. package/studio-plugin/package-lock.json +692 -0
  12. package/studio-plugin/package.json +19 -0
  13. package/studio-plugin/plugin.json +10 -0
  14. package/studio-plugin/src/modules/ClientBroker.ts +447 -0
  15. package/studio-plugin/src/modules/Communication.ts +697 -0
  16. package/studio-plugin/src/modules/EvalBridges.ts +255 -0
  17. package/studio-plugin/src/modules/HttpDiagnostics.ts +50 -0
  18. package/studio-plugin/src/modules/JobRegistry.ts +77 -0
  19. package/studio-plugin/src/modules/LuauExec.ts +465 -0
  20. package/studio-plugin/src/modules/Recording.ts +28 -0
  21. package/studio-plugin/src/modules/RenderMonitor.ts +60 -0
  22. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +152 -0
  23. package/studio-plugin/src/modules/ServerUrlSettings.ts +114 -0
  24. package/studio-plugin/src/modules/State.ts +101 -0
  25. package/studio-plugin/src/modules/StopPlayMonitor.ts +276 -0
  26. package/studio-plugin/src/modules/UI.ts +790 -0
  27. package/studio-plugin/src/modules/Utils.ts +318 -0
  28. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
  29. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +460 -0
  30. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
  31. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +203 -0
  32. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +149 -0
  33. package/studio-plugin/src/modules/handlers/InputHandlers.ts +160 -0
  34. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
  35. package/studio-plugin/src/modules/handlers/JobHandlers.ts +111 -0
  36. package/studio-plugin/src/modules/handlers/LogHandlers.ts +14 -0
  37. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +44 -0
  38. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +354 -0
  39. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +1263 -0
  40. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
  41. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +809 -0
  42. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +216 -0
  43. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
  44. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +386 -0
  45. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +172 -0
  46. package/studio-plugin/src/modules/handlers/TestHandlers.ts +535 -0
  47. package/studio-plugin/src/server/index.server.ts +134 -0
  48. package/studio-plugin/src/types/index.d.ts +83 -0
  49. package/studio-plugin/tsconfig.json +21 -0
@@ -0,0 +1,1263 @@
1
+ import { RunService } from "@rbxts/services";
2
+
3
+ interface LibMPControl {
4
+ EnableProfiler(this: LibMPControl, enable: boolean): boolean;
5
+ EnableCapture(this: LibMPControl, enable: boolean): boolean;
6
+ CaptureToBufferSync(this: LibMPControl): buffer;
7
+ IsBackendAccessible(this: LibMPControl): boolean;
8
+ IsBackendReady(this: LibMPControl): boolean;
9
+ IsBackendVersionCompatible(this: LibMPControl): boolean;
10
+ }
11
+
12
+ interface LibMPLike {
13
+ Control: LibMPControl;
14
+ Session: {
15
+ OpenFromBuffer(this: void, data: buffer): LibMPSession | undefined;
16
+ };
17
+ Versions?: Record<string, unknown>;
18
+ GetMemUsed?: () => number;
19
+ }
20
+
21
+ interface LibMPSession {
22
+ IsValid(this: LibMPSession): boolean;
23
+ SyncWithDataSource(this: LibMPSession): boolean;
24
+ GetDataFormatVersion(this: LibMPSession): number;
25
+ GetObjSize(this: LibMPSession): number;
26
+ FetchTimerIds(this: LibMPSession): number[];
27
+ FetchThreadIds(this: LibMPSession): number[];
28
+ FetchThreadDesc(this: LibMPSession, threadId: number): ThreadDesc | undefined;
29
+ GetFrameIdMin(this: LibMPSession): number;
30
+ GetFrameIdMax(this: LibMPSession): number;
31
+ GetFrameDesc(this: LibMPSession, frameId: number): FrameDesc | undefined;
32
+ FetchTimerDesc(this: LibMPSession, timerId: number): TimerDesc | undefined;
33
+ FetchGroupDesc(this: LibMPSession, groupId: number): GroupDesc | undefined;
34
+ FindGroupIds(this: LibMPSession, nameMask: string, caseSensitive?: boolean): number[];
35
+ FindTimerIds(this: LibMPSession, nameMask: string, caseSensitive?: boolean): number[];
36
+ CreateLogIterator(this: LibMPSession): LogIterator;
37
+ Dispose(this: LibMPSession): void;
38
+ }
39
+
40
+ interface TimerDesc {
41
+ TimerId?: number;
42
+ TimerName?: string;
43
+ GroupId?: number;
44
+ IsUserTimer?: boolean;
45
+ }
46
+
47
+ interface GroupDesc {
48
+ GroupId?: number;
49
+ GroupName?: string;
50
+ IsGpu?: boolean;
51
+ }
52
+
53
+ interface ThreadDesc {
54
+ ThreadId?: number;
55
+ ThreadName?: string;
56
+ BufferSize?: number;
57
+ IsGpu?: boolean;
58
+ }
59
+
60
+ interface FrameDesc {
61
+ FrameId(this: FrameDesc): number;
62
+ TickStartCpu(this: FrameDesc): number;
63
+ TickEndCpu(this: FrameDesc): number;
64
+ IsIncomplete(this: FrameDesc): boolean;
65
+ IsPaused(this: FrameDesc): boolean;
66
+ }
67
+
68
+ interface LogIterator {
69
+ Configure(this: LogIterator, config: Record<string, unknown>): void;
70
+ Step(this: LogIterator): boolean;
71
+ GetState(this: LogIterator): LogIteratorState | undefined;
72
+ Dispose(this: LogIterator): void;
73
+ }
74
+
75
+ interface LogIteratorState {
76
+ FrameId(this: LogIteratorState): number;
77
+ ThreadId(this: LogIteratorState): number;
78
+ TimerId(this: LogIteratorState): number;
79
+ Timestamp(this: LogIteratorState): number;
80
+ IsEnter(this: LogIteratorState): boolean;
81
+ IsExit(this: LogIteratorState): boolean;
82
+ }
83
+
84
+ interface StackEntry {
85
+ timerId: number;
86
+ timestampRaw: number;
87
+ childRaw: number;
88
+ frameId: number;
89
+ }
90
+
91
+ interface TimerInfo {
92
+ timer_id: number;
93
+ name: string;
94
+ group_id?: number;
95
+ group?: string;
96
+ is_user_timer?: boolean;
97
+ }
98
+
99
+ interface TimerAggregate {
100
+ timer_id: number;
101
+ inclusive_raw: number;
102
+ exclusive_raw: number;
103
+ count: number;
104
+ max_raw: number;
105
+ }
106
+
107
+ interface GroupAggregate {
108
+ group: string;
109
+ inclusive_raw: number;
110
+ exclusive_raw: number;
111
+ count: number;
112
+ }
113
+
114
+ interface EdgeAggregate {
115
+ parent_timer_id: number;
116
+ child_timer_id: number;
117
+ inclusive_raw: number;
118
+ count: number;
119
+ max_raw: number;
120
+ }
121
+
122
+ const DEFAULT_DURATION_MS = 1000;
123
+ const MIN_DURATION_MS = 100;
124
+ const MAX_DURATION_MS = 5000;
125
+ const DEFAULT_MAX_TIMERS = 20;
126
+ const DEFAULT_MAX_GROUPS = 20;
127
+ const DEFAULT_MAX_TIMERS_PER_GROUP = 5;
128
+ const DEFAULT_MAX_RELATED_TIMERS = 3;
129
+ const DEFAULT_MAX_EVENTS = 250000;
130
+ const MAX_EVENTS = 1000000;
131
+ const DEFAULT_FRAME_WINDOW = 240;
132
+ const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
133
+ const PAD_BYTE = string.byte("=")[0];
134
+
135
+ const B64: number[] = [];
136
+ for (let i = 0; i < 64; i++) {
137
+ B64[i] = string.byte(BASE64_CHARS, i + 1)[0];
138
+ }
139
+
140
+ const FOCUS_GROUP_MASKS: Record<string, string[]> = {
141
+ all: [],
142
+ script: ["Script", "LuaBridge"],
143
+ physics: ["Physics"],
144
+ render: ["Render"],
145
+ network: ["Network", "RbxTransport", "Replicator"],
146
+ jobs: ["Jobs"],
147
+ };
148
+
149
+ let cachedLibMP: LibMPLike | undefined;
150
+
151
+ function normalizeDurationMs(value: unknown): number {
152
+ if (!typeIs(value, "number")) return DEFAULT_DURATION_MS;
153
+ return math.clamp(math.floor(value), MIN_DURATION_MS, MAX_DURATION_MS);
154
+ }
155
+
156
+ function normalizeMaxTimers(value: unknown): number {
157
+ if (!typeIs(value, "number")) return DEFAULT_MAX_TIMERS;
158
+ return math.clamp(math.floor(value), 1, 100);
159
+ }
160
+
161
+ function normalizeMaxGroups(value: unknown): number {
162
+ if (!typeIs(value, "number")) return DEFAULT_MAX_GROUPS;
163
+ return math.clamp(math.floor(value), 1, 100);
164
+ }
165
+
166
+ function normalizeMaxTimersPerGroup(value: unknown): number {
167
+ if (!typeIs(value, "number")) return DEFAULT_MAX_TIMERS_PER_GROUP;
168
+ return math.clamp(math.floor(value), 0, 20);
169
+ }
170
+
171
+ function normalizeMaxRelatedTimers(value: unknown): number {
172
+ if (!typeIs(value, "number")) return DEFAULT_MAX_RELATED_TIMERS;
173
+ return math.clamp(math.floor(value), 0, 10);
174
+ }
175
+
176
+ function normalizeMaxEvents(value: unknown): number {
177
+ if (!typeIs(value, "number")) return DEFAULT_MAX_EVENTS;
178
+ return math.clamp(math.floor(value), 10000, MAX_EVENTS);
179
+ }
180
+
181
+ function normalizeFrameWindow(value: unknown): number {
182
+ if (!typeIs(value, "number")) return DEFAULT_FRAME_WINDOW;
183
+ return math.clamp(math.floor(value), 1, 2000);
184
+ }
185
+
186
+ function normalizeMinTotalUs(value: unknown): number {
187
+ if (!typeIs(value, "number")) return 0;
188
+ return math.max(0, value);
189
+ }
190
+
191
+ function normalizeFocus(value: unknown): string {
192
+ if (!typeIs(value, "string") || FOCUS_GROUP_MASKS[value] === undefined) return "all";
193
+ return value;
194
+ }
195
+
196
+ function stringContains(haystack: string, needle: string): boolean {
197
+ return string.find(string.lower(haystack), string.lower(needle), 1, true)[0] !== undefined;
198
+ }
199
+
200
+ function rawToUs(raw: number): number {
201
+ return math.floor(raw / 1000 + 0.5);
202
+ }
203
+
204
+ function round2(value: number): number {
205
+ return math.floor(value * 100 + 0.5) / 100;
206
+ }
207
+
208
+ function perSecond(value: number, durationMs: number): number {
209
+ return durationMs > 0 ? round2(value / (durationMs / 1000)) : value;
210
+ }
211
+
212
+ function percent(part: number, whole: number): number {
213
+ return whole > 0 ? round2((part / whole) * 100) : 0;
214
+ }
215
+
216
+ function ratio(numerator: number, denominator: number): number | undefined {
217
+ if (denominator <= 0) return undefined;
218
+ return round2(numerator / denominator);
219
+ }
220
+
221
+ function percentile(sortedValues: number[], fraction: number): number {
222
+ if (sortedValues.size() === 0) return 0;
223
+ const index = math.clamp(math.ceil(sortedValues.size() * fraction), 1, sortedValues.size()) - 1;
224
+ return sortedValues[index];
225
+ }
226
+
227
+ function copyRecord(row: Record<string, unknown>): Record<string, unknown> {
228
+ const out: Record<string, unknown> = {};
229
+ for (const [key, value] of pairs(row)) {
230
+ out[key as string] = value;
231
+ }
232
+ return out;
233
+ }
234
+
235
+ function pickFields(row: Record<string, unknown>, fields: string[]): Record<string, unknown> {
236
+ const out: Record<string, unknown> = {};
237
+ for (const field of fields) {
238
+ const value = row[field];
239
+ if (value !== undefined) out[field] = value;
240
+ }
241
+ return out;
242
+ }
243
+
244
+ function addFrameRaw(map: Map<number, number>, frameId: number, raw: number): void {
245
+ if (frameId <= 0) return;
246
+ map.set(frameId, (map.get(frameId) ?? 0) + raw);
247
+ }
248
+
249
+ function summarizeFrameImpact(frameRawById: Map<number, number> | undefined, analyzedFrames: number): Record<string, unknown> {
250
+ if (frameRawById === undefined || frameRawById.size() === 0) {
251
+ return {
252
+ active_frame_count: 0,
253
+ active_frame_pct: 0,
254
+ inclusive_us_per_frame: 0,
255
+ p95_active_frame_inclusive_us: 0,
256
+ max_frame_inclusive_us: 0,
257
+ };
258
+ }
259
+
260
+ const values: number[] = [];
261
+ let totalUs = 0;
262
+ let maxUs = 0;
263
+ let maxFrameId = 0;
264
+ for (const [frameId, raw] of frameRawById) {
265
+ const us = rawToUs(raw);
266
+ values.push(us);
267
+ totalUs += us;
268
+ if (us > maxUs) {
269
+ maxUs = us;
270
+ maxFrameId = frameId;
271
+ }
272
+ }
273
+ values.sort((a, b) => a < b);
274
+
275
+ return {
276
+ active_frame_count: values.size(),
277
+ active_frame_pct: percent(values.size(), analyzedFrames),
278
+ inclusive_us_per_frame: analyzedFrames > 0 ? round2(totalUs / analyzedFrames) : totalUs,
279
+ avg_active_frame_inclusive_us: values.size() > 0 ? round2(totalUs / values.size()) : 0,
280
+ p95_active_frame_inclusive_us: percentile(values, 0.95),
281
+ max_frame_inclusive_us: maxUs,
282
+ max_frame_id: maxFrameId,
283
+ };
284
+ }
285
+
286
+ function encodeBase64(buf: buffer): string {
287
+ const len = buffer.len(buf);
288
+ const fullTriples = math.floor(len / 3);
289
+ const remaining = len - fullTriples * 3;
290
+ const outLen = (fullTriples + (remaining > 0 ? 1 : 0)) * 4;
291
+ const out = buffer.create(outLen);
292
+
293
+ let si = 0;
294
+ let di = 0;
295
+
296
+ for (let t = 0; t < fullTriples; t++) {
297
+ const b0 = buffer.readu8(buf, si);
298
+ const b1 = buffer.readu8(buf, si + 1);
299
+ const b2 = buffer.readu8(buf, si + 2);
300
+
301
+ buffer.writeu8(out, di, B64[bit32.rshift(b0, 2)]);
302
+ buffer.writeu8(out, di + 1, B64[bit32.bor(bit32.lshift(bit32.band(b0, 3), 4), bit32.rshift(b1, 4))]);
303
+ buffer.writeu8(out, di + 2, B64[bit32.bor(bit32.lshift(bit32.band(b1, 15), 2), bit32.rshift(b2, 6))]);
304
+ buffer.writeu8(out, di + 3, B64[bit32.band(b2, 63)]);
305
+
306
+ si += 3;
307
+ di += 4;
308
+ }
309
+
310
+ if (remaining === 2) {
311
+ const b0 = buffer.readu8(buf, si);
312
+ const b1 = buffer.readu8(buf, si + 1);
313
+ buffer.writeu8(out, di, B64[bit32.rshift(b0, 2)]);
314
+ buffer.writeu8(out, di + 1, B64[bit32.bor(bit32.lshift(bit32.band(b0, 3), 4), bit32.rshift(b1, 4))]);
315
+ buffer.writeu8(out, di + 2, B64[bit32.lshift(bit32.band(b1, 15), 2)]);
316
+ buffer.writeu8(out, di + 3, PAD_BYTE);
317
+ } else if (remaining === 1) {
318
+ const b0 = buffer.readu8(buf, si);
319
+ buffer.writeu8(out, di, B64[bit32.rshift(b0, 2)]);
320
+ buffer.writeu8(out, di + 1, B64[bit32.lshift(bit32.band(b0, 3), 4)]);
321
+ buffer.writeu8(out, di + 2, PAD_BYTE);
322
+ buffer.writeu8(out, di + 3, PAD_BYTE);
323
+ }
324
+
325
+ return buffer.tostring(out);
326
+ }
327
+
328
+ function requireLibMP(): LibMPLike | Record<string, unknown> {
329
+ if (cachedLibMP !== undefined) return cachedLibMP;
330
+ const includeFolder = script.Parent!.Parent!.Parent!.FindFirstChild("include");
331
+ const libModule = includeFolder && includeFolder.FindFirstChild("LibMP");
332
+ if (!libModule || !libModule.IsA("ModuleScript")) {
333
+ return {
334
+ error: "libmp_missing",
335
+ message: "The MCP plugin bundle does not contain include/LibMP.",
336
+ };
337
+ }
338
+ const [ok, libOrErr] = pcall(() => require(libModule) as LibMPLike);
339
+ if (!ok) {
340
+ return {
341
+ error: "libmp_require_failed",
342
+ message: tostring(libOrErr),
343
+ };
344
+ }
345
+ cachedLibMP = libOrErr as LibMPLike;
346
+ return cachedLibMP;
347
+ }
348
+
349
+ function safeCall<T>(fn: () => T): LuaTuple<[boolean, T | string]> {
350
+ const [ok, value] = pcall(fn);
351
+ if (ok) return $tuple(true, value as T);
352
+ return $tuple(false, tostring(value));
353
+ }
354
+
355
+ function isIdleTimer(info: TimerInfo): boolean {
356
+ const name = string.lower(info.name);
357
+ if (name === "sleep" || name === "idle") return true;
358
+ if (string.find(name, "sleep", 1, true)[0] !== undefined) return true;
359
+ return false;
360
+ }
361
+
362
+ function recommendedToolsForGroups(groups: Record<string, unknown>[], targetRole: string): Record<string, unknown>[] {
363
+ const tools: Record<string, unknown>[] = [];
364
+ const seen = new Set<string>();
365
+ for (const row of groups) {
366
+ const group = row.group;
367
+ if (!typeIs(group, "string") || seen.has(group)) continue;
368
+ seen.add(group);
369
+ if (group === "Script" || group === "LuaBridge") {
370
+ tools.push({
371
+ tool: "capture_script_profiler",
372
+ arguments: { target: targetRole, duration_ms: 1000 },
373
+ reason: "Script/LuaBridge timers are present.",
374
+ });
375
+ } else if (group === "Physics") {
376
+ tools.push({ tool: "get_scene_analysis", reason: "Physics timers are present; inspect scene complexity." });
377
+ } else if (group === "Render") {
378
+ tools.push({ tool: "capture_screenshot", reason: "Render timers are present; inspect visible scene/UI state." });
379
+ } else if (group === "Network" || group === "RbxTransport" || group === "Replicator") {
380
+ tools.push({ tool: "get_runtime_logs", reason: "Network/replication timers are present; correlate with gameplay events." });
381
+ } else if (group === "Jobs") {
382
+ tools.push({ tool: "capture_micro_profiler", arguments: { target: targetRole, focus: "jobs" }, reason: "Jobs timers are present; narrow to job lanes if needed." });
383
+ }
384
+ if (tools.size() >= 3) break;
385
+ }
386
+ return tools;
387
+ }
388
+
389
+ function collectFrameSummary(session: LibMPSession, startFrame: number, frameMax: number): Record<string, unknown> {
390
+ const frameRows: Record<string, unknown>[] = [];
391
+ const durations: number[] = [];
392
+ let incompleteFrames = 0;
393
+ let pausedFrames = 0;
394
+
395
+ for (let frameId = startFrame; frameId <= frameMax; frameId++) {
396
+ const [ok, descOrErr] = safeCall(() => session.GetFrameDesc(frameId));
397
+ if (!ok || !descOrErr) continue;
398
+ const desc = descOrErr as FrameDesc;
399
+ const tickStart = desc.TickStartCpu();
400
+ const tickEnd = desc.TickEndCpu();
401
+ const durationRaw = tickEnd - tickStart;
402
+ const isIncomplete = desc.IsIncomplete();
403
+ const isPaused = desc.IsPaused();
404
+ if (isIncomplete) incompleteFrames += 1;
405
+ if (isPaused) pausedFrames += 1;
406
+ if (durationRaw <= 0 || durationRaw > 1000000000000) continue;
407
+ const durationUs = rawToUs(durationRaw);
408
+ durations.push(durationUs);
409
+ const row: Record<string, unknown> = {
410
+ frame_id: frameId,
411
+ duration_us: durationUs,
412
+ };
413
+ if (isIncomplete) row.incomplete = true;
414
+ if (isPaused) row.paused = true;
415
+ frameRows.push(row);
416
+ }
417
+
418
+ durations.sort((a, b) => a < b);
419
+ frameRows.sort((a, b) => (a.duration_us as number) > (b.duration_us as number));
420
+
421
+ let totalUs = 0;
422
+ for (const duration of durations) totalUs += duration;
423
+
424
+ const topFrames: Record<string, unknown>[] = [];
425
+ for (let i = 0; i < math.min(10, frameRows.size()); i++) {
426
+ topFrames.push(frameRows[i]);
427
+ }
428
+
429
+ const count = durations.size();
430
+ return {
431
+ frames: count,
432
+ total_duration_us: totalUs,
433
+ avg_us: count > 0 ? round2(totalUs / count) : 0,
434
+ p50_us: percentile(durations, 0.5),
435
+ p95_us: percentile(durations, 0.95),
436
+ max_us: count > 0 ? durations[count - 1] : 0,
437
+ incomplete_frames: incompleteFrames,
438
+ paused_frames: pausedFrames,
439
+ top_frames: topFrames,
440
+ };
441
+ }
442
+
443
+ function captureMicroProfiler(requestData: Record<string, unknown>): unknown {
444
+ if (!RunService.IsRunning()) {
445
+ return {
446
+ error: "runtime_target_required",
447
+ message: "MicroProfiler capture requires a running playtest target such as target=\"server\" or target=\"client-1\".",
448
+ };
449
+ }
450
+
451
+ const libOrError = requireLibMP();
452
+ if ((libOrError as { Control?: unknown }).Control === undefined) return libOrError;
453
+ const LibMP = libOrError as LibMPLike;
454
+
455
+ const durationMs = normalizeDurationMs(requestData.duration_ms);
456
+ const maxTimers = normalizeMaxTimers(requestData.max_timers);
457
+ const maxGroups = normalizeMaxGroups(requestData.max_groups);
458
+ const maxTimersPerGroup = normalizeMaxTimersPerGroup(requestData.max_timers_per_group);
459
+ const maxRelatedTimers = normalizeMaxRelatedTimers(requestData.max_related_timers);
460
+ const maxEvents = normalizeMaxEvents(requestData.max_events);
461
+ const frameWindow = normalizeFrameWindow(requestData.frame_window);
462
+ const minTotalUs = normalizeMinTotalUs(requestData.min_total_us);
463
+ const focus = normalizeFocus(requestData.focus);
464
+ const filter = typeIs(requestData.filter, "string") && requestData.filter !== "" ? requestData.filter as string : undefined;
465
+ const includeIdle = requestData.include_idle === true;
466
+ const includeGpu = requestData.include_gpu === true;
467
+ const includeRawBuffer = requestData.__mcp_include_raw_buffer === true;
468
+ const includeComparisonIndex = requestData.__mcp_include_comparison_index === true;
469
+ const targetRole = typeIs(requestData.__mcp_target_role, "string") ? requestData.__mcp_target_role as string : "runtime";
470
+
471
+ const backend = {
472
+ accessible: LibMP.Control.IsBackendAccessible(),
473
+ ready: LibMP.Control.IsBackendReady(),
474
+ compatible: LibMP.Control.IsBackendVersionCompatible(),
475
+ versions: LibMP.Versions,
476
+ };
477
+ if (!backend.accessible || !backend.ready || !backend.compatible) {
478
+ return {
479
+ error: "micro_profiler_backend_unavailable",
480
+ message: "MicroProfilerService backend is not accessible, ready, and compatible in this runtime peer.",
481
+ backend,
482
+ };
483
+ }
484
+
485
+ const [profilerOk, profilerResult] = safeCall(() => LibMP.Control.EnableProfiler(true));
486
+ if (!profilerOk) {
487
+ return {
488
+ error: "micro_profiler_enable_failed",
489
+ message: tostring(profilerResult),
490
+ backend,
491
+ };
492
+ }
493
+
494
+ const [captureStartOk, captureStartResult] = safeCall(() => LibMP.Control.EnableCapture(true));
495
+ if (!captureStartOk) {
496
+ return {
497
+ error: "micro_profiler_capture_start_failed",
498
+ message: tostring(captureStartResult),
499
+ backend,
500
+ };
501
+ }
502
+
503
+ task.wait(durationMs / 1000);
504
+
505
+ const [captureStopOk, captureStopResult] = safeCall(() => LibMP.Control.EnableCapture(false));
506
+ if (!captureStopOk) {
507
+ return {
508
+ error: "micro_profiler_capture_stop_failed",
509
+ message: tostring(captureStopResult),
510
+ backend,
511
+ };
512
+ }
513
+
514
+ const [bufferOk, snapshotOrErr] = safeCall(() => LibMP.Control.CaptureToBufferSync());
515
+ if (!bufferOk) {
516
+ return {
517
+ error: "micro_profiler_snapshot_failed",
518
+ message: tostring(snapshotOrErr),
519
+ backend,
520
+ };
521
+ }
522
+ const snapshot = snapshotOrErr as buffer;
523
+
524
+ const [sessionOk, sessionOrErr] = safeCall(() => LibMP.Session.OpenFromBuffer(snapshot));
525
+ if (!sessionOk || !sessionOrErr) {
526
+ return {
527
+ error: "micro_profiler_decode_failed",
528
+ message: tostring(sessionOrErr),
529
+ buffer_bytes: buffer.len(snapshot),
530
+ backend,
531
+ };
532
+ }
533
+ const session = sessionOrErr as LibMPSession;
534
+ if (!session.IsValid()) {
535
+ session.Dispose();
536
+ return {
537
+ error: "micro_profiler_session_invalid",
538
+ buffer_bytes: buffer.len(snapshot),
539
+ backend,
540
+ };
541
+ }
542
+
543
+ const timerIds = session.FetchTimerIds() ?? [];
544
+ const threadIds = session.FetchThreadIds() ?? [];
545
+ const frameMin = session.GetFrameIdMin();
546
+ const frameMax = session.GetFrameIdMax();
547
+ const startFrame = math.max(frameMin, frameMax - frameWindow + 1);
548
+ const framesConsidered = frameMax >= startFrame ? frameMax - startFrame + 1 : 0;
549
+
550
+ const groupCache = new Map<number, string>();
551
+ const timerCache = new Map<number, TimerInfo>();
552
+ const threadCache = new Map<number, Record<string, unknown>>();
553
+
554
+ function getGroupName(groupId: number | undefined): string | undefined {
555
+ if (groupId === undefined) return undefined;
556
+ const cached = groupCache.get(groupId);
557
+ if (cached !== undefined) return cached;
558
+ const [ok, descOrErr] = safeCall(() => session.FetchGroupDesc(groupId));
559
+ const desc = ok ? descOrErr as GroupDesc | undefined : undefined;
560
+ const name = desc && typeIs(desc.GroupName, "string") && desc.GroupName !== "" ? desc.GroupName : tostring(groupId);
561
+ groupCache.set(groupId, name);
562
+ return name;
563
+ }
564
+
565
+ function getTimerInfo(timerId: number): TimerInfo {
566
+ const cached = timerCache.get(timerId);
567
+ if (cached !== undefined) return cached;
568
+ const [ok, descOrErr] = safeCall(() => session.FetchTimerDesc(timerId));
569
+ const desc = ok ? descOrErr as TimerDesc | undefined : undefined;
570
+ const groupId = desc && typeIs(desc.GroupId, "number") ? desc.GroupId : undefined;
571
+ const info: TimerInfo = {
572
+ timer_id: timerId,
573
+ name: desc && typeIs(desc.TimerName, "string") && desc.TimerName !== "" ? desc.TimerName : tostring(timerId),
574
+ group_id: groupId,
575
+ group: getGroupName(groupId),
576
+ is_user_timer: desc && desc.IsUserTimer === true ? true : undefined,
577
+ };
578
+ timerCache.set(timerId, info);
579
+ return info;
580
+ }
581
+
582
+ function getThreadInfo(threadId: number): Record<string, unknown> {
583
+ const cached = threadCache.get(threadId);
584
+ if (cached !== undefined) return cached;
585
+ const [ok, descOrErr] = safeCall(() => session.FetchThreadDesc(threadId));
586
+ const desc = ok ? descOrErr as ThreadDesc | undefined : undefined;
587
+ const info: Record<string, unknown> = {
588
+ thread_id: threadId,
589
+ name: desc && typeIs(desc.ThreadName, "string") && desc.ThreadName !== "" ? desc.ThreadName : tostring(threadId),
590
+ };
591
+ if (desc && desc.IsGpu === true) info.is_gpu = true;
592
+ if (desc && typeIs(desc.BufferSize, "number")) info.buffer_size = desc.BufferSize;
593
+ threadCache.set(threadId, info);
594
+ return info;
595
+ }
596
+
597
+ function addTimerAggregate(map: Map<number, TimerAggregate>, timerId: number, inclusiveRaw: number, exclusiveRaw: number): void {
598
+ let aggregate = map.get(timerId);
599
+ if (aggregate === undefined) {
600
+ aggregate = { timer_id: timerId, inclusive_raw: 0, exclusive_raw: 0, count: 0, max_raw: 0 };
601
+ map.set(timerId, aggregate);
602
+ }
603
+ aggregate.inclusive_raw += inclusiveRaw;
604
+ aggregate.exclusive_raw += exclusiveRaw;
605
+ aggregate.count += 1;
606
+ if (inclusiveRaw > aggregate.max_raw) aggregate.max_raw = inclusiveRaw;
607
+ }
608
+
609
+ const focusMasks = FOCUS_GROUP_MASKS[focus] ?? [];
610
+ const focusGroupIds: number[] = [];
611
+ for (const mask of focusMasks) {
612
+ const [ok, idsOrErr] = safeCall(() => session.FindGroupIds(mask, false));
613
+ if (ok && typeIs(idsOrErr, "table")) {
614
+ for (const id of idsOrErr as number[]) {
615
+ if (!focusGroupIds.includes(id)) focusGroupIds.push(id);
616
+ }
617
+ }
618
+ }
619
+
620
+ const iteratorConfig: Record<string, unknown> = {
621
+ StartFrameId: startFrame,
622
+ EndFrameId: frameMax,
623
+ SkipGpuThreads: !includeGpu,
624
+ SkipEvents: true,
625
+ SkipPausedFrames: true,
626
+ SkipFrameBoundaries: true,
627
+ };
628
+ if (focusGroupIds.size() > 0) iteratorConfig.GroupIds = focusGroupIds;
629
+
630
+ const iterator = session.CreateLogIterator();
631
+ const [configOk, configErr] = safeCall(() => {
632
+ iterator.Configure(iteratorConfig);
633
+ return true;
634
+ });
635
+ if (!configOk) {
636
+ iterator.Dispose();
637
+ session.Dispose();
638
+ return {
639
+ error: "micro_profiler_iterator_config_failed",
640
+ message: tostring(configErr),
641
+ backend,
642
+ };
643
+ }
644
+
645
+ const stacks = new Map<number, StackEntry[]>();
646
+ const aggregates = new Map<number, TimerAggregate>();
647
+ const timerThreadAggregates = new Map<number, Map<number, TimerAggregate>>();
648
+ const timerFrameAggregates = new Map<number, Map<number, number>>();
649
+ const edgeAggregates = new Map<string, EdgeAggregate>();
650
+ const sampledFrames = new Set<number>();
651
+ let eventsSampled = 0;
652
+ let enterEvents = 0;
653
+ let exitEvents = 0;
654
+ let unmatchedExits = 0;
655
+ let droppedSpans = 0;
656
+ let sampledFrameMin: number | undefined;
657
+ let sampledFrameMax: number | undefined;
658
+ let lastProcessedFrameId: number | undefined;
659
+ let lastProcessedTimestampRaw: number | undefined;
660
+
661
+ while (eventsSampled < maxEvents && iterator.Step()) {
662
+ eventsSampled += 1;
663
+ const state = iterator.GetState();
664
+ if (!state) continue;
665
+ const frameId = state.FrameId();
666
+ const threadId = state.ThreadId();
667
+ const timerId = state.TimerId();
668
+ const timestampRaw = state.Timestamp();
669
+ lastProcessedFrameId = frameId;
670
+ lastProcessedTimestampRaw = timestampRaw;
671
+ if (frameId > 0) {
672
+ sampledFrames.add(frameId);
673
+ if (sampledFrameMin === undefined || frameId < sampledFrameMin) sampledFrameMin = frameId;
674
+ if (sampledFrameMax === undefined || frameId > sampledFrameMax) sampledFrameMax = frameId;
675
+ }
676
+ let stack = stacks.get(threadId);
677
+ if (stack === undefined) {
678
+ stack = [];
679
+ stacks.set(threadId, stack);
680
+ }
681
+
682
+ if (state.IsEnter()) {
683
+ enterEvents += 1;
684
+ stack.push({ timerId, timestampRaw, childRaw: 0, frameId });
685
+ } else if (state.IsExit()) {
686
+ exitEvents += 1;
687
+ let entry: StackEntry | undefined;
688
+ while (stack.size() > 0) {
689
+ const candidate = stack.pop();
690
+ if (candidate && candidate.timerId === timerId) {
691
+ entry = candidate;
692
+ break;
693
+ }
694
+ }
695
+ if (entry === undefined) {
696
+ unmatchedExits += 1;
697
+ continue;
698
+ }
699
+ const inclusiveRaw = timestampRaw - entry.timestampRaw;
700
+ if (inclusiveRaw < 0 || inclusiveRaw > 1000000000000) {
701
+ droppedSpans += 1;
702
+ continue;
703
+ }
704
+ const exclusiveRaw = math.max(0, inclusiveRaw - entry.childRaw);
705
+ const parent = stack[stack.size() - 1];
706
+ if (parent !== undefined) {
707
+ parent.childRaw += inclusiveRaw;
708
+ const edgeKey = `${parent.timerId}:${timerId}`;
709
+ let edge = edgeAggregates.get(edgeKey);
710
+ if (edge === undefined) {
711
+ edge = { parent_timer_id: parent.timerId, child_timer_id: timerId, inclusive_raw: 0, count: 0, max_raw: 0 };
712
+ edgeAggregates.set(edgeKey, edge);
713
+ }
714
+ edge.inclusive_raw += inclusiveRaw;
715
+ edge.count += 1;
716
+ if (inclusiveRaw > edge.max_raw) edge.max_raw = inclusiveRaw;
717
+ }
718
+
719
+ addTimerAggregate(aggregates, timerId, inclusiveRaw, exclusiveRaw);
720
+ let frameMap = timerFrameAggregates.get(timerId);
721
+ if (frameMap === undefined) {
722
+ frameMap = new Map<number, number>();
723
+ timerFrameAggregates.set(timerId, frameMap);
724
+ }
725
+ addFrameRaw(frameMap, entry.frameId > 0 ? entry.frameId : frameId, inclusiveRaw);
726
+ let threadMap = timerThreadAggregates.get(timerId);
727
+ if (threadMap === undefined) {
728
+ threadMap = new Map<number, TimerAggregate>();
729
+ timerThreadAggregates.set(timerId, threadMap);
730
+ }
731
+ addTimerAggregate(threadMap, threadId, inclusiveRaw, exclusiveRaw);
732
+ }
733
+ }
734
+
735
+ const sampledFrameCoveragePct = percent(sampledFrames.size(), framesConsidered);
736
+ const analysisFrameMin = sampledFrameMin ?? startFrame;
737
+ const analysisFrameMax = sampledFrameMax ?? frameMax;
738
+ const frameSummary = collectFrameSummary(session, analysisFrameMin, analysisFrameMax);
739
+ const analysisFrameCount = typeIs(frameSummary.frames, "number") && (frameSummary.frames as number) > 0
740
+ ? frameSummary.frames as number
741
+ : sampledFrames.size() > 0
742
+ ? sampledFrames.size()
743
+ : framesConsidered;
744
+ const analysisDurationUs = typeIs(frameSummary.total_duration_us, "number") && (frameSummary.total_duration_us as number) > 0
745
+ ? frameSummary.total_duration_us as number
746
+ : durationMs * 1000;
747
+ const analysisDurationMs = analysisDurationUs / 1000;
748
+
749
+ const rows: Record<string, unknown>[] = [];
750
+ const rowsByTimerId = new Map<number, Record<string, unknown>>();
751
+ const groupAggregates = new Map<string, GroupAggregate>();
752
+ const groupTimerRows = new Map<string, Record<string, unknown>[]>();
753
+ const groupFrameAggregates = new Map<string, Map<number, number>>();
754
+ const threadAggregates = new Map<number, TimerAggregate>();
755
+ const threadTimerRows = new Map<number, Record<string, unknown>[]>();
756
+ let omittedIdle = 0;
757
+ let omittedBelowThreshold = 0;
758
+ let omittedByFilter = 0;
759
+
760
+ for (const [timerId, aggregate] of aggregates) {
761
+ const info = getTimerInfo(timerId);
762
+ const totalUs = rawToUs(aggregate.inclusive_raw);
763
+ if (!includeIdle && isIdleTimer(info)) {
764
+ omittedIdle += 1;
765
+ continue;
766
+ }
767
+ if (totalUs < minTotalUs) {
768
+ omittedBelowThreshold += 1;
769
+ continue;
770
+ }
771
+ if (filter !== undefined) {
772
+ const searchable = `${info.name} ${info.group ?? ""}`;
773
+ if (!stringContains(searchable, filter)) {
774
+ omittedByFilter += 1;
775
+ continue;
776
+ }
777
+ }
778
+
779
+ const group = info.group ?? "<unknown>";
780
+ const inclusiveUs = rawToUs(aggregate.inclusive_raw);
781
+ const exclusiveUs = rawToUs(aggregate.exclusive_raw);
782
+ const avgUs = aggregate.count > 0 ? round2(totalUs / aggregate.count) : 0;
783
+ const maxUs = rawToUs(aggregate.max_raw);
784
+ let groupAgg = groupAggregates.get(group);
785
+ if (groupAgg === undefined) {
786
+ groupAgg = { group, inclusive_raw: 0, exclusive_raw: 0, count: 0 };
787
+ groupAggregates.set(group, groupAgg);
788
+ }
789
+ groupAgg.inclusive_raw += aggregate.inclusive_raw;
790
+ groupAgg.exclusive_raw += aggregate.exclusive_raw;
791
+ groupAgg.count += aggregate.count;
792
+
793
+ const row: Record<string, unknown> = {
794
+ timer_id: timerId,
795
+ name: info.name,
796
+ group,
797
+ inclusive_us: inclusiveUs,
798
+ inclusive_us_per_s: perSecond(inclusiveUs, analysisDurationMs),
799
+ exclusive_us: exclusiveUs,
800
+ exclusive_pct: percent(exclusiveUs, inclusiveUs),
801
+ count: aggregate.count,
802
+ count_per_s: perSecond(aggregate.count, analysisDurationMs),
803
+ count_per_frame: analysisFrameCount > 0 ? round2(aggregate.count / analysisFrameCount) : aggregate.count,
804
+ avg_invocation_us: avgUs,
805
+ max_invocation_us: maxUs,
806
+ pct_of_analyzed_wall: percent(inclusiveUs, analysisDurationUs),
807
+ };
808
+ const maxToAvg = ratio(maxUs, avgUs);
809
+ if (maxToAvg !== undefined) row.max_to_avg = maxToAvg;
810
+ const timerFrameImpact = summarizeFrameImpact(timerFrameAggregates.get(timerId), analysisFrameCount);
811
+ for (const [key, value] of pairs(timerFrameImpact)) {
812
+ row[key as string] = value;
813
+ }
814
+ if (info.is_user_timer === true) row.is_user_timer = true;
815
+ rows.push(row);
816
+ rowsByTimerId.set(timerId, row);
817
+ let timerRows = groupTimerRows.get(group);
818
+ if (timerRows === undefined) {
819
+ timerRows = [];
820
+ groupTimerRows.set(group, timerRows);
821
+ }
822
+ timerRows.push(row);
823
+ const timerFrames = timerFrameAggregates.get(timerId);
824
+ if (timerFrames !== undefined) {
825
+ let groupFrames = groupFrameAggregates.get(group);
826
+ if (groupFrames === undefined) {
827
+ groupFrames = new Map<number, number>();
828
+ groupFrameAggregates.set(group, groupFrames);
829
+ }
830
+ for (const [frameId, raw] of timerFrames) addFrameRaw(groupFrames, frameId, raw);
831
+ }
832
+
833
+ const perThread = timerThreadAggregates.get(timerId);
834
+ if (perThread !== undefined) {
835
+ const timerThreadSummaryRows: Record<string, unknown>[] = [];
836
+ for (const [threadId, threadAggregate] of perThread) {
837
+ let threadAggregateRow = threadAggregates.get(threadId);
838
+ if (threadAggregateRow === undefined) {
839
+ threadAggregateRow = { timer_id: threadId, inclusive_raw: 0, exclusive_raw: 0, count: 0, max_raw: 0 };
840
+ threadAggregates.set(threadId, threadAggregateRow);
841
+ }
842
+ threadAggregateRow.inclusive_raw += threadAggregate.inclusive_raw;
843
+ threadAggregateRow.exclusive_raw += threadAggregate.exclusive_raw;
844
+ threadAggregateRow.count += threadAggregate.count;
845
+ if (threadAggregate.max_raw > threadAggregateRow.max_raw) threadAggregateRow.max_raw = threadAggregate.max_raw;
846
+
847
+ let threadRows = threadTimerRows.get(threadId);
848
+ if (threadRows === undefined) {
849
+ threadRows = [];
850
+ threadTimerRows.set(threadId, threadRows);
851
+ }
852
+ const threadTotalUs = rawToUs(threadAggregate.inclusive_raw);
853
+ const threadExclusiveUs = rawToUs(threadAggregate.exclusive_raw);
854
+ const threadAvgUs = threadAggregate.count > 0 ? round2(threadTotalUs / threadAggregate.count) : 0;
855
+ const threadInfo = getThreadInfo(threadId);
856
+ const threadTimerRow: Record<string, unknown> = {
857
+ thread_id: threadId,
858
+ thread_name: threadInfo.name,
859
+ is_gpu: threadInfo.is_gpu,
860
+ timer_id: timerId,
861
+ name: info.name,
862
+ group,
863
+ inclusive_us: threadTotalUs,
864
+ inclusive_us_per_s: perSecond(threadTotalUs, analysisDurationMs),
865
+ exclusive_us: threadExclusiveUs,
866
+ count: threadAggregate.count,
867
+ count_per_s: perSecond(threadAggregate.count, analysisDurationMs),
868
+ avg_invocation_us: threadAvgUs,
869
+ max_invocation_us: rawToUs(threadAggregate.max_raw),
870
+ };
871
+ const threadMaxToAvg = ratio(threadTimerRow.max_invocation_us as number, threadAvgUs);
872
+ if (threadMaxToAvg !== undefined) threadTimerRow.max_to_avg = threadMaxToAvg;
873
+ threadRows.push(threadTimerRow);
874
+ timerThreadSummaryRows.push({
875
+ thread_id: threadId,
876
+ thread_name: threadInfo.name,
877
+ is_gpu: threadInfo.is_gpu,
878
+ inclusive_us: threadTotalUs,
879
+ exclusive_us: threadExclusiveUs,
880
+ count: threadAggregate.count,
881
+ });
882
+ }
883
+ timerThreadSummaryRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
884
+ if (maxRelatedTimers > 0 && timerThreadSummaryRows.size() > 0) {
885
+ const topTimerThreads: Record<string, unknown>[] = [];
886
+ for (let i = 0; i < math.min(maxRelatedTimers, timerThreadSummaryRows.size()); i++) topTimerThreads.push(timerThreadSummaryRows[i]);
887
+ row.top_threads = topTimerThreads;
888
+ }
889
+ }
890
+ }
891
+
892
+ rows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
893
+
894
+ const edgeRows: Record<string, unknown>[] = [];
895
+ const parentRelationsByChild = new Map<number, Record<string, unknown>[]>();
896
+ const childRelationsByParent = new Map<number, Record<string, unknown>[]>();
897
+ for (const [, edge] of edgeAggregates) {
898
+ const parentRow = rowsByTimerId.get(edge.parent_timer_id);
899
+ const childRow = rowsByTimerId.get(edge.child_timer_id);
900
+ if (parentRow === undefined || childRow === undefined) continue;
901
+ const inclusiveUs = rawToUs(edge.inclusive_raw);
902
+ const maxUs = rawToUs(edge.max_raw);
903
+ const avgUs = edge.count > 0 ? round2(inclusiveUs / edge.count) : 0;
904
+ const edgeRow: Record<string, unknown> = {
905
+ parent: {
906
+ timer_id: edge.parent_timer_id,
907
+ name: parentRow.name,
908
+ group: parentRow.group,
909
+ },
910
+ child: {
911
+ timer_id: edge.child_timer_id,
912
+ name: childRow.name,
913
+ group: childRow.group,
914
+ },
915
+ inclusive_us: inclusiveUs,
916
+ inclusive_us_per_s: perSecond(inclusiveUs, analysisDurationMs),
917
+ count: edge.count,
918
+ count_per_s: perSecond(edge.count, analysisDurationMs),
919
+ avg_invocation_us: avgUs,
920
+ max_invocation_us: maxUs,
921
+ };
922
+ const maxToAvg = ratio(maxUs, avgUs);
923
+ if (maxToAvg !== undefined) edgeRow.max_to_avg = maxToAvg;
924
+ edgeRows.push(edgeRow);
925
+
926
+ let parentRelations = parentRelationsByChild.get(edge.child_timer_id);
927
+ if (parentRelations === undefined) {
928
+ parentRelations = [];
929
+ parentRelationsByChild.set(edge.child_timer_id, parentRelations);
930
+ }
931
+ parentRelations.push({
932
+ timer_id: edge.parent_timer_id,
933
+ name: parentRow.name,
934
+ group: parentRow.group,
935
+ inclusive_us: inclusiveUs,
936
+ count: edge.count,
937
+ pct_of_timer_inclusive: percent(inclusiveUs, childRow.inclusive_us as number),
938
+ });
939
+
940
+ let childRelations = childRelationsByParent.get(edge.parent_timer_id);
941
+ if (childRelations === undefined) {
942
+ childRelations = [];
943
+ childRelationsByParent.set(edge.parent_timer_id, childRelations);
944
+ }
945
+ childRelations.push({
946
+ timer_id: edge.child_timer_id,
947
+ name: childRow.name,
948
+ group: childRow.group,
949
+ inclusive_us: inclusiveUs,
950
+ count: edge.count,
951
+ pct_of_timer_inclusive: percent(inclusiveUs, parentRow.inclusive_us as number),
952
+ });
953
+ }
954
+ edgeRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
955
+ if (maxRelatedTimers > 0) {
956
+ for (const [, relationRows] of parentRelationsByChild) {
957
+ relationRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
958
+ }
959
+ for (const [, relationRows] of childRelationsByParent) {
960
+ relationRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
961
+ }
962
+ for (const row of rows) {
963
+ const timerId = row.timer_id as number;
964
+ const parents = parentRelationsByChild.get(timerId);
965
+ const children = childRelationsByParent.get(timerId);
966
+ if (parents !== undefined && parents.size() > 0) {
967
+ const topParents: Record<string, unknown>[] = [];
968
+ for (let i = 0; i < math.min(maxRelatedTimers, parents.size()); i++) topParents.push(parents[i]);
969
+ row.top_parents = topParents;
970
+ }
971
+ if (children !== undefined && children.size() > 0) {
972
+ const topChildren: Record<string, unknown>[] = [];
973
+ for (let i = 0; i < math.min(maxRelatedTimers, children.size()); i++) topChildren.push(children[i]);
974
+ row.top_children = topChildren;
975
+ }
976
+ }
977
+ }
978
+
979
+ const topTimers: Record<string, unknown>[] = [];
980
+ for (let i = 0; i < math.min(maxTimers, rows.size()); i++) {
981
+ const row = copyRecord(rows[i]);
982
+ row.rank = i + 1;
983
+ topTimers.push(row);
984
+ }
985
+
986
+ const rowsByExclusive: Record<string, unknown>[] = [];
987
+ for (const row of rows) rowsByExclusive.push(row);
988
+ rowsByExclusive.sort((a, b) => (a.exclusive_us as number) > (b.exclusive_us as number));
989
+ const topTimersByExclusive: Record<string, unknown>[] = [];
990
+ for (let i = 0; i < math.min(maxTimers, rowsByExclusive.size()); i++) {
991
+ const row = copyRecord(rowsByExclusive[i]);
992
+ row.rank = i + 1;
993
+ topTimersByExclusive.push(row);
994
+ }
995
+
996
+ const topEdges: Record<string, unknown>[] = [];
997
+ for (let i = 0; i < math.min(maxTimers, edgeRows.size()); i++) {
998
+ const row = copyRecord(edgeRows[i]);
999
+ row.rank = i + 1;
1000
+ topEdges.push(row);
1001
+ }
1002
+
1003
+ const threadRows: Record<string, unknown>[] = [];
1004
+ for (const [threadId, aggregate] of threadAggregates) {
1005
+ const info = getThreadInfo(threadId);
1006
+ const inclusiveUs = rawToUs(aggregate.inclusive_raw);
1007
+ const exclusiveUs = rawToUs(aggregate.exclusive_raw);
1008
+ const row: Record<string, unknown> = {
1009
+ thread_id: threadId,
1010
+ thread_name: info.name,
1011
+ is_gpu: info.is_gpu,
1012
+ inclusive_us: inclusiveUs,
1013
+ inclusive_us_per_s: perSecond(inclusiveUs, analysisDurationMs),
1014
+ exclusive_us: exclusiveUs,
1015
+ exclusive_pct: percent(exclusiveUs, inclusiveUs),
1016
+ count: aggregate.count,
1017
+ count_per_s: perSecond(aggregate.count, analysisDurationMs),
1018
+ max_invocation_us: rawToUs(aggregate.max_raw),
1019
+ pct_of_analyzed_wall: percent(inclusiveUs, analysisDurationUs),
1020
+ };
1021
+ const timerRows = threadTimerRows.get(threadId) ?? [];
1022
+ timerRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
1023
+ const topThreadTimers: Record<string, unknown>[] = [];
1024
+ for (let i = 0; i < math.min(maxTimersPerGroup, timerRows.size()); i++) {
1025
+ const timerRow = copyRecord(timerRows[i]);
1026
+ topThreadTimers.push(timerRow);
1027
+ }
1028
+ if (topThreadTimers.size() > 0) row.top_timers = topThreadTimers;
1029
+ threadRows.push(row);
1030
+ }
1031
+ threadRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
1032
+ const topThreads: Record<string, unknown>[] = [];
1033
+ for (let i = 0; i < math.min(maxGroups, threadRows.size()); i++) {
1034
+ const row = threadRows[i];
1035
+ row.rank = i + 1;
1036
+ topThreads.push(row);
1037
+ }
1038
+
1039
+ const groupRows: Record<string, unknown>[] = [];
1040
+ for (const [, aggregate] of groupAggregates) {
1041
+ const timerRows = groupTimerRows.get(aggregate.group) ?? [];
1042
+ timerRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
1043
+ const topGroupTimers: Record<string, unknown>[] = [];
1044
+ for (let i = 0; i < math.min(maxTimersPerGroup, timerRows.size()); i++) {
1045
+ const timerRow = timerRows[i];
1046
+ topGroupTimers.push({
1047
+ timer_id: timerRow.timer_id,
1048
+ name: timerRow.name,
1049
+ inclusive_us: timerRow.inclusive_us,
1050
+ inclusive_us_per_s: timerRow.inclusive_us_per_s,
1051
+ exclusive_us: timerRow.exclusive_us,
1052
+ count: timerRow.count,
1053
+ max_invocation_us: timerRow.max_invocation_us,
1054
+ max_frame_inclusive_us: timerRow.max_frame_inclusive_us,
1055
+ });
1056
+ }
1057
+ const inclusiveUs = rawToUs(aggregate.inclusive_raw);
1058
+ const exclusiveUs = rawToUs(aggregate.exclusive_raw);
1059
+ const groupRow: Record<string, unknown> = {
1060
+ group: aggregate.group,
1061
+ inclusive_us: inclusiveUs,
1062
+ inclusive_us_per_s: perSecond(inclusiveUs, analysisDurationMs),
1063
+ exclusive_us: exclusiveUs,
1064
+ exclusive_pct: percent(exclusiveUs, inclusiveUs),
1065
+ count: aggregate.count,
1066
+ count_per_s: perSecond(aggregate.count, analysisDurationMs),
1067
+ count_per_frame: analysisFrameCount > 0 ? round2(aggregate.count / analysisFrameCount) : aggregate.count,
1068
+ timer_count: timerRows.size(),
1069
+ pct_of_analyzed_wall: percent(inclusiveUs, analysisDurationUs),
1070
+ };
1071
+ const groupFrameImpact = summarizeFrameImpact(groupFrameAggregates.get(aggregate.group), analysisFrameCount);
1072
+ for (const [key, value] of pairs(groupFrameImpact)) {
1073
+ groupRow[key as string] = value;
1074
+ }
1075
+ if (topGroupTimers.size() > 0) groupRow.top_timers = topGroupTimers;
1076
+ groupRows.push(groupRow);
1077
+ }
1078
+ groupRows.sort((a, b) => (a.inclusive_us as number) > (b.inclusive_us as number));
1079
+ const topGroups: Record<string, unknown>[] = [];
1080
+ for (let i = 0; i < math.min(maxGroups, groupRows.size()); i++) {
1081
+ const row = copyRecord(groupRows[i]);
1082
+ row.rank = i + 1;
1083
+ topGroups.push(row);
1084
+ }
1085
+
1086
+ const groupRowsByExclusive: Record<string, unknown>[] = [];
1087
+ for (const row of groupRows) groupRowsByExclusive.push(row);
1088
+ groupRowsByExclusive.sort((a, b) => (a.exclusive_us as number) > (b.exclusive_us as number));
1089
+ const topGroupsByExclusive: Record<string, unknown>[] = [];
1090
+ for (let i = 0; i < math.min(maxGroups, groupRowsByExclusive.size()); i++) {
1091
+ const row = copyRecord(groupRowsByExclusive[i]);
1092
+ row.rank = i + 1;
1093
+ topGroupsByExclusive.push(row);
1094
+ }
1095
+
1096
+ const omitted: Record<string, number> = {};
1097
+ if (omittedIdle > 0) omitted.idle = omittedIdle;
1098
+ if (omittedBelowThreshold > 0) omitted.below_min_total_us = omittedBelowThreshold;
1099
+ if (omittedByFilter > 0) omitted.filtered_out = omittedByFilter;
1100
+
1101
+ let openStackEntriesAtEnd = 0;
1102
+ for (const [, stack] of stacks) openStackEntriesAtEnd += stack.size();
1103
+ const iteratorFinished = eventsSampled < maxEvents;
1104
+ const partialReasons: string[] = [];
1105
+ if (eventsSampled >= maxEvents) partialReasons.push("event_limit_hit");
1106
+ if (openStackEntriesAtEnd > 0) partialReasons.push("open_stack_entries_at_end");
1107
+ if (sampledFrameCoveragePct < 100) partialReasons.push("selected_frame_event_coverage_below_100");
1108
+ let comparisonIndex: Record<string, unknown> | undefined;
1109
+ if (includeComparisonIndex) {
1110
+ const timerFields = [
1111
+ "timer_id",
1112
+ "name",
1113
+ "group",
1114
+ "inclusive_us",
1115
+ "inclusive_us_per_s",
1116
+ "exclusive_us",
1117
+ "count",
1118
+ "count_per_s",
1119
+ "active_frame_count",
1120
+ "inclusive_us_per_frame",
1121
+ "max_frame_inclusive_us",
1122
+ "max_frame_id",
1123
+ ];
1124
+ const groupFields = [
1125
+ "group",
1126
+ "inclusive_us",
1127
+ "inclusive_us_per_s",
1128
+ "exclusive_us",
1129
+ "count",
1130
+ "timer_count",
1131
+ "active_frame_count",
1132
+ "inclusive_us_per_frame",
1133
+ "max_frame_inclusive_us",
1134
+ "max_frame_id",
1135
+ ];
1136
+ const threadFields = [
1137
+ "thread_id",
1138
+ "thread_name",
1139
+ "is_gpu",
1140
+ "inclusive_us",
1141
+ "inclusive_us_per_s",
1142
+ "exclusive_us",
1143
+ "count",
1144
+ ];
1145
+ const edgeFields = [
1146
+ "parent",
1147
+ "child",
1148
+ "inclusive_us",
1149
+ "inclusive_us_per_s",
1150
+ "count",
1151
+ "max_invocation_us",
1152
+ ];
1153
+ const timerIndex: Record<string, unknown>[] = [];
1154
+ for (const row of rows) timerIndex.push(pickFields(row, timerFields));
1155
+ const groupIndex: Record<string, unknown>[] = [];
1156
+ for (const row of groupRows) groupIndex.push(pickFields(row, groupFields));
1157
+ const threadIndex: Record<string, unknown>[] = [];
1158
+ for (const row of threadRows) threadIndex.push(pickFields(row, threadFields));
1159
+ const edgeIndex: Record<string, unknown>[] = [];
1160
+ for (const row of edgeRows) edgeIndex.push(pickFields(row, edgeFields));
1161
+ comparisonIndex = {
1162
+ timers: timerIndex,
1163
+ groups: groupIndex,
1164
+ threads: threadIndex,
1165
+ call_edges: edgeIndex,
1166
+ };
1167
+ }
1168
+
1169
+ const result: Record<string, unknown> = {
1170
+ schema_version: 1,
1171
+ ok: true,
1172
+ duration_ms: durationMs,
1173
+ target: targetRole,
1174
+ scope: "micro_profiler",
1175
+ time_unit: "microseconds",
1176
+ time_basis: "LibMP MicroProfiler timestamps converted from nanosecond ticks. inclusive_us is cumulative nested timer time and can overlap across nested timers/threads; do not sum rows as total frame time.",
1177
+ analysis_window: {
1178
+ requested_duration_ms: durationMs,
1179
+ analysis_duration_us: analysisDurationUs,
1180
+ snapshot_frame_min: frameMin,
1181
+ snapshot_frame_max: frameMax,
1182
+ selected_frame_min: startFrame,
1183
+ selected_frame_max: frameMax,
1184
+ selected_frame_count: framesConsidered,
1185
+ analyzed_frame_min: analysisFrameMin,
1186
+ analyzed_frame_max: analysisFrameMax,
1187
+ analyzed_frame_count: analysisFrameCount,
1188
+ processed_frame_min: sampledFrameMin,
1189
+ processed_frame_max: sampledFrameMax,
1190
+ processed_frame_count: sampledFrames.size(),
1191
+ selected_frame_event_coverage_pct: sampledFrameCoveragePct,
1192
+ frame_window: frameWindow,
1193
+ },
1194
+ applied: {
1195
+ focus,
1196
+ filter: filter ?? undefined,
1197
+ include_idle: includeIdle,
1198
+ include_gpu: includeGpu,
1199
+ min_total_us: minTotalUs,
1200
+ max_groups: maxGroups,
1201
+ max_timers: maxTimers,
1202
+ max_timers_per_group: maxTimersPerGroup,
1203
+ max_related_timers: maxRelatedTimers,
1204
+ max_events: maxEvents,
1205
+ frame_window: frameWindow,
1206
+ sort: "inclusive_us_desc",
1207
+ },
1208
+ counts: {
1209
+ buffer_bytes: buffer.len(snapshot),
1210
+ timers: timerIds.size(),
1211
+ threads: threadIds.size(),
1212
+ frame_min: frameMin,
1213
+ frame_max: frameMax,
1214
+ frames_considered: framesConsidered,
1215
+ sampled_frame_min: sampledFrameMin,
1216
+ sampled_frame_max: sampledFrameMax,
1217
+ sampled_frames: sampledFrames.size(),
1218
+ events_sampled: eventsSampled,
1219
+ enter_events: enterEvents,
1220
+ exit_events: exitEvents,
1221
+ unmatched_exits: unmatchedExits,
1222
+ dropped_spans: droppedSpans,
1223
+ open_stack_entries_at_end: openStackEntriesAtEnd,
1224
+ iterator_finished: iteratorFinished,
1225
+ last_processed_frame_id: lastProcessedFrameId,
1226
+ last_processed_timestamp_us: lastProcessedTimestampRaw !== undefined ? rawToUs(lastProcessedTimestampRaw) : undefined,
1227
+ event_limit_hit: eventsSampled >= maxEvents,
1228
+ },
1229
+ frame_summary: frameSummary,
1230
+ top_groups: topGroups,
1231
+ top_groups_by_exclusive: topGroupsByExclusive,
1232
+ top_threads: topThreads,
1233
+ top_call_edges: topEdges,
1234
+ top_timers: topTimers,
1235
+ top_timers_by_exclusive: topTimersByExclusive,
1236
+ data_quality: {
1237
+ event_limit_hit: eventsSampled >= maxEvents,
1238
+ iterator_finished: iteratorFinished,
1239
+ unmatched_exits: unmatchedExits,
1240
+ dropped_spans: droppedSpans,
1241
+ open_stack_entries_at_end: openStackEntriesAtEnd,
1242
+ selected_frame_event_coverage_pct: sampledFrameCoveragePct,
1243
+ partial: partialReasons.size() > 0,
1244
+ partial_reasons: partialReasons,
1245
+ notes: focus !== "all"
1246
+ ? ["focus filters events before stack aggregation; exclusive_us is exclusive within emitted focused events, not the full snapshot."]
1247
+ : undefined,
1248
+ },
1249
+ recommended_tools: recommendedToolsForGroups(topGroups, targetRole),
1250
+ };
1251
+ if (next(omitted)[0] !== undefined) result.omitted = omitted;
1252
+ if (comparisonIndex !== undefined) result.comparison_index = comparisonIndex;
1253
+ result.backend = backend;
1254
+ if (session.GetDataFormatVersion() !== undefined) result.libmp_data_format_version = session.GetDataFormatVersion();
1255
+ if (session.GetObjSize() !== undefined) result.snapshot_object_size_bytes = session.GetObjSize();
1256
+ if (includeRawBuffer) result.raw_snapshot_base64 = encodeBase64(snapshot);
1257
+
1258
+ iterator.Dispose();
1259
+ session.Dispose();
1260
+ return result;
1261
+ }
1262
+
1263
+ export = { captureMicroProfiler };