@forgeax/engine-profiler 0.1.4 → 0.1.7
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/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser-user-timing.d.ts.map +1 -1
- package/dist/browser-user-timing.mjs +1 -6
- package/dist/browser-user-timing.mjs.map +1 -1
- package/dist/cli.d.ts +2 -14
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.mjs +1 -14
- package/dist/cli.mjs.map +1 -1
- package/dist/compare.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +14 -51
- package/dist/index.mjs.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-profile-types.mjs +3 -12
- package/src/__tests__/cli-input-errors.test.ts +0 -39
- package/src/__tests__/cli-process.integration.test.ts +1 -28
- package/src/browser-user-timing.ts +3 -11
- package/src/cli.ts +6 -21
- package/src/clock.ts +1 -1
- package/src/compare.ts +1 -4
- package/src/index.ts +0 -6
- package/src/recorder.ts +15 -16
- package/dist/__tests__/browser-profile-detail-owner.test-d.d.ts +0 -2
- package/dist/__tests__/browser-profile-detail-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/browser-user-timing.test.d.ts +0 -2
- package/dist/__tests__/browser-user-timing.test.d.ts.map +0 -1
- package/dist/__tests__/comparison-side-owner.test-d.d.ts +0 -2
- package/dist/__tests__/comparison-side-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/schema-derived-union-owner.test.d.ts +0 -2
- package/dist/__tests__/schema-derived-union-owner.test.d.ts.map +0 -1
- package/dist/tool-preview-profile.d.ts +0 -33
- package/dist/tool-preview-profile.d.ts.map +0 -1
- package/src/__tests__/browser-profile-detail-owner.test-d.ts +0 -26
- package/src/__tests__/browser-user-timing.test.ts +0 -127
- package/src/__tests__/comparison-side-owner.test-d.ts +0 -28
- package/src/__tests__/schema-derived-union-owner.test.ts +0 -57
- package/src/tool-preview-profile.ts +0 -67
package/dist/recorder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAiB,KAAK,cAAc,EAAc,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAEjB,gBAAgB,EAChB,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;AAEtF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC3D,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACvE,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IACjC,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC1D,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,cAAc,CAAC,cAAc,CAAC,CAAC;CAC1C;AAgDD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAAC,IAAI,CAAC,CAKnF;AAgLD,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,EACnB,YAAY,EAAE,oBAAoB,EAClC,gBAAgB,CAAC,EAAE;IAAE,8BAA8B,EAAE,MAAM,CAAA;CAAE,GAC5D,cAAc,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAiB,KAAK,cAAc,EAAc,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAEjB,gBAAgB,EAChB,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CACjC;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;AAEtF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAClD,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC3D,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACvE,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IACjC,UAAU,CAAC,KAAK,EAAE,gBAAgB,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC1D,QAAQ,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,cAAc,CAAC,cAAc,CAAC,CAAC;CAC1C;AAgDD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAAC,IAAI,CAAC,CAKnF;AAgLD,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,YAAY,EACnB,YAAY,EAAE,oBAAoB,EAClC,gBAAgB,CAAC,EAAE;IAAE,8BAA8B,EAAE,MAAM,CAAA;CAAE,GAC5D,cAAc,CAAC,eAAe,CAAC,CAsJjC"}
|
package/package.json
CHANGED
|
@@ -4,22 +4,13 @@ const packageRoot = new URL('../', import.meta.url);
|
|
|
4
4
|
const schemaPath = new URL('./schema/profile-capture.schema.json', packageRoot);
|
|
5
5
|
const outputPath = new URL('./src/generated/profile-capture.ts', packageRoot);
|
|
6
6
|
const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
|
|
7
|
-
const literalUnion = (values) => values.map((value) => `'${value}'`).join(' | ');
|
|
8
|
-
const resolveDefinition = (reference) => schema.$defs[reference.slice('#/$defs/'.length)];
|
|
9
|
-
const profileSource = literalUnion(schema.$defs.source.enum);
|
|
10
|
-
const profileRecordKind = literalUnion(
|
|
11
|
-
schema.$defs.record.oneOf.map(
|
|
12
|
-
({ $ref }) => resolveDefinition($ref).properties.kind.const,
|
|
13
|
-
),
|
|
14
|
-
);
|
|
15
|
-
const profileCompletenessStatus = literalUnion(schema.$defs.completeness.properties.status.enum);
|
|
16
7
|
|
|
17
8
|
const output = `// Generated by scripts/generate-profile-types.mjs. Do not edit.
|
|
18
9
|
// Schema version: ${schema.properties.schemaVersion.const}
|
|
19
10
|
|
|
20
|
-
export type ProfileSource =
|
|
21
|
-
export type ProfileRecordKind =
|
|
22
|
-
export type ProfileCompletenessStatus =
|
|
11
|
+
export type ProfileSource = 'app' | 'render';
|
|
12
|
+
export type ProfileRecordKind = 'phase' | 'skip';
|
|
13
|
+
export type ProfileCompletenessStatus = 'complete' | 'partial' | 'overflow';
|
|
23
14
|
|
|
24
15
|
export interface ProfilePhaseRecord {
|
|
25
16
|
readonly kind: 'phase';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
1
|
import { fileURLToPath } from 'node:url';
|
|
3
2
|
import { describe, expect, it } from 'vitest';
|
|
4
3
|
import { readCliInput, runProfilerCli } from '../cli.js';
|
|
@@ -56,44 +55,6 @@ describe('profiler CLI input error contract', () => {
|
|
|
56
55
|
expect(readCliInput(['summary'], () => 'capture')).toBe('capture');
|
|
57
56
|
});
|
|
58
57
|
|
|
59
|
-
it('returns a structured reader failure and retries the same reader deterministically', () => {
|
|
60
|
-
const input = readFileSync(
|
|
61
|
-
new URL('./fixtures/cli/valid-capture.json', import.meta.url),
|
|
62
|
-
'utf8',
|
|
63
|
-
);
|
|
64
|
-
let refuse = true;
|
|
65
|
-
const readStdin = (): string => {
|
|
66
|
-
if (refuse) {
|
|
67
|
-
refuse = false;
|
|
68
|
-
throw new Error('EISDIR: illegal operation on a directory, read');
|
|
69
|
-
}
|
|
70
|
-
return input;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const first = runProfilerCli(['summary'], readCliInput(['summary'], readStdin));
|
|
74
|
-
expectStructuredError(first, 'cli-input-stdin-read-failed');
|
|
75
|
-
expect(JSON.parse(first.stderr)).toEqual({
|
|
76
|
-
error: {
|
|
77
|
-
code: 'cli-input-stdin-read-failed',
|
|
78
|
-
expected: expect.any(String),
|
|
79
|
-
hint: expect.any(String),
|
|
80
|
-
detail: {
|
|
81
|
-
message: 'EISDIR: illegal operation on a directory, read',
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const second = runProfilerCli(['summary'], readCliInput(['summary'], readStdin));
|
|
87
|
-
const repeat = runProfilerCli(['summary'], readCliInput(['summary'], readStdin));
|
|
88
|
-
expect(second.exitCode).toBe(0);
|
|
89
|
-
expect(second.stderr).toBe('');
|
|
90
|
-
expect(second.stdout).toBe(repeat.stdout);
|
|
91
|
-
expect(JSON.parse(second.stdout)).toMatchObject({
|
|
92
|
-
query: 'summary',
|
|
93
|
-
captureId: 'capture-0042',
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
58
|
it('rejects empty stdin without producing a summary', () => {
|
|
98
59
|
expectStructuredError(runProfilerCli(['summary'], ''), 'cli-input-empty');
|
|
99
60
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { describe, expect, it } from 'vitest';
|
|
5
5
|
|
|
@@ -23,18 +23,6 @@ function runCli(args: readonly string[], input: string) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function runCliWithDirectoryStdin(args: readonly string[]) {
|
|
27
|
-
const stdin = openSync(fileURLToPath(new URL('./', import.meta.url)), 'r');
|
|
28
|
-
try {
|
|
29
|
-
return spawnSync(process.execPath, [cliPath, ...args], {
|
|
30
|
-
stdio: [stdin, 'pipe', 'pipe'],
|
|
31
|
-
encoding: 'utf8',
|
|
32
|
-
});
|
|
33
|
-
} finally {
|
|
34
|
-
closeSync(stdin);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
26
|
function parseObject(output: string): Record<string, unknown> {
|
|
39
27
|
return JSON.parse(output) as Record<string, unknown>;
|
|
40
28
|
}
|
|
@@ -84,21 +72,6 @@ describe('profiler CLI public process contract', () => {
|
|
|
84
72
|
});
|
|
85
73
|
});
|
|
86
74
|
|
|
87
|
-
it('returns one structured stdin reader error through the built entry', () => {
|
|
88
|
-
const refusal = runCliWithDirectoryStdin(['summary']);
|
|
89
|
-
|
|
90
|
-
expect(refusal.status).toBe(2);
|
|
91
|
-
expect(refusal.stdout).toBe('');
|
|
92
|
-
expect(parseObject(refusal.stderr)).toEqual({
|
|
93
|
-
error: {
|
|
94
|
-
code: 'cli-input-stdin-read-failed',
|
|
95
|
-
expected: expect.any(String),
|
|
96
|
-
hint: expect.any(String),
|
|
97
|
-
detail: { message: expect.stringContaining('EISDIR') },
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
75
|
it('compares two file artifacts deterministically through the built entry', () => {
|
|
103
76
|
const args = ['compare', '--left-file', fixturePath, '--right-file', partialFixturePath];
|
|
104
77
|
const first = runCli(args, 'stdin must not be read');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Profiler } from './profiler.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RecorderSession } from './recorder.js';
|
|
3
3
|
import type { ProfileCapture, ProfilePhaseStart, ProfileSource } from './types.js';
|
|
4
4
|
|
|
5
5
|
/** The small browser API surface used by the opt-in User Timing adapter. */
|
|
@@ -17,15 +17,7 @@ export interface UserTimingProfilerOptions {
|
|
|
17
17
|
|
|
18
18
|
type DiagnosticGlobal = Record<string, unknown>;
|
|
19
19
|
type PhaseCatalog = ProfileCapture['phaseCatalog'];
|
|
20
|
-
type BrowserProfileDetail =
|
|
21
|
-
const DEFAULT_CAPTURE_ID = 'capture-0001';
|
|
22
|
-
const CAPTURE_ID_PATTERN = /^capture-[0-9]{4,}$/;
|
|
23
|
-
|
|
24
|
-
function resolveCaptureId(captureId: string | undefined): string {
|
|
25
|
-
return captureId === undefined || CAPTURE_ID_PATTERN.test(captureId)
|
|
26
|
-
? (captureId ?? DEFAULT_CAPTURE_ID)
|
|
27
|
-
: DEFAULT_CAPTURE_ID;
|
|
28
|
-
}
|
|
20
|
+
type BrowserProfileDetail = 'owner' | 'passes' | 'nested';
|
|
29
21
|
|
|
30
22
|
function partialCapture(captureId: string, phaseCatalog: PhaseCatalog): ProfileCapture {
|
|
31
23
|
return {
|
|
@@ -73,7 +65,7 @@ export function createUserTimingProfiler(
|
|
|
73
65
|
options: UserTimingProfilerOptions = {},
|
|
74
66
|
): Profiler | undefined {
|
|
75
67
|
const diagnosticsKey = options.diagnosticsKey ?? '__forgeaxFramePhaseDiagnostics';
|
|
76
|
-
const captureId =
|
|
68
|
+
const captureId = options.captureId ?? 'forgeax-user-timing';
|
|
77
69
|
const timingEnabled = diagnosticsEnabled(diagnosticsKey);
|
|
78
70
|
const detail = diagnosticsDetail(diagnosticsKey);
|
|
79
71
|
const performanceApi = timingEnabled
|
package/src/cli.ts
CHANGED
|
@@ -31,7 +31,6 @@ type CliError = {
|
|
|
31
31
|
| 'cli-input-empty'
|
|
32
32
|
| 'cli-input-file-read-failed'
|
|
33
33
|
| 'cli-input-invalid-json'
|
|
34
|
-
| 'cli-input-stdin-read-failed'
|
|
35
34
|
| 'cli-query-invalid';
|
|
36
35
|
readonly expected: string;
|
|
37
36
|
readonly hint: string;
|
|
@@ -250,7 +249,7 @@ function parseArguments(args: readonly string[]): CliArguments | CliError {
|
|
|
250
249
|
return validateCommand(state) ?? toCliArguments(state);
|
|
251
250
|
}
|
|
252
251
|
|
|
253
|
-
function readArtifact(filePath: string | undefined, stdin: string
|
|
252
|
+
function readArtifact(filePath: string | undefined, stdin: string): string | CliError {
|
|
254
253
|
if (filePath !== undefined) {
|
|
255
254
|
try {
|
|
256
255
|
return readFileSync(filePath, 'utf8');
|
|
@@ -266,7 +265,6 @@ function readArtifact(filePath: string | undefined, stdin: string | CliError): s
|
|
|
266
265
|
};
|
|
267
266
|
}
|
|
268
267
|
}
|
|
269
|
-
if (isCliError(stdin)) return stdin;
|
|
270
268
|
if (stdin.trim().length === 0) {
|
|
271
269
|
return {
|
|
272
270
|
code: 'cli-input-empty',
|
|
@@ -352,10 +350,7 @@ function projectComparison(value: ProfileComparisonProjection): Record<string, u
|
|
|
352
350
|
return { query: 'compare', ...value };
|
|
353
351
|
}
|
|
354
352
|
|
|
355
|
-
export function runProfilerCli(
|
|
356
|
-
args: readonly string[],
|
|
357
|
-
stdin: string | CliError,
|
|
358
|
-
): ProfilerCliResult {
|
|
353
|
+
export function runProfilerCli(args: readonly string[], stdin: string): ProfilerCliResult {
|
|
359
354
|
const parsedArguments = parseArguments(args);
|
|
360
355
|
if ('code' in parsedArguments) {
|
|
361
356
|
return { stdout: '', stderr: output({ error: parsedArguments }), exitCode: 2 };
|
|
@@ -390,20 +385,10 @@ export function runProfilerCli(
|
|
|
390
385
|
};
|
|
391
386
|
}
|
|
392
387
|
|
|
393
|
-
export function readCliInput(args: readonly string[], readStdin: () => string): string
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
return readStdin();
|
|
399
|
-
} catch (error) {
|
|
400
|
-
return {
|
|
401
|
-
code: 'cli-input-stdin-read-failed',
|
|
402
|
-
expected: 'stdin can be read as one ProfileCapture JSON object',
|
|
403
|
-
hint: 'Provide readable ProfileCapture JSON on stdin or pass --file <path>.',
|
|
404
|
-
detail: { message: error instanceof Error ? error.message : String(error) },
|
|
405
|
-
};
|
|
406
|
-
}
|
|
388
|
+
export function readCliInput(args: readonly string[], readStdin: () => string): string {
|
|
389
|
+
return args.some((argument) => ['--file', '--left-file', '--right-file'].includes(argument))
|
|
390
|
+
? ''
|
|
391
|
+
: readStdin();
|
|
407
392
|
}
|
|
408
393
|
|
|
409
394
|
export function main(): void {
|
package/src/clock.ts
CHANGED
|
@@ -7,7 +7,7 @@ export function createProfileClock(readMicros: () => number = defaultReadMicros)
|
|
|
7
7
|
return {
|
|
8
8
|
nowMicros() {
|
|
9
9
|
const currentMicros = readMicros();
|
|
10
|
-
|
|
10
|
+
lastMicros = Math.max(lastMicros, currentMicros);
|
|
11
11
|
return lastMicros;
|
|
12
12
|
},
|
|
13
13
|
};
|
package/src/compare.ts
CHANGED
|
@@ -50,10 +50,7 @@ export type ProfileComparisonError = ProfileArtifactError & {
|
|
|
50
50
|
};
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
function withSide(
|
|
54
|
-
side: ProfileComparisonError['detail']['side'],
|
|
55
|
-
error: ProfileArtifactError,
|
|
56
|
-
): ProfileComparisonError {
|
|
53
|
+
function withSide(side: 'left' | 'right', error: ProfileArtifactError): ProfileComparisonError {
|
|
57
54
|
return {
|
|
58
55
|
...error,
|
|
59
56
|
detail: { ...error.detail, side },
|
package/src/index.ts
CHANGED
|
@@ -34,12 +34,6 @@ export { createProfiler } from './profiler.js';
|
|
|
34
34
|
export type { ProfileDetail, RecorderSession } from './recorder.js';
|
|
35
35
|
/** Validates a persisted capture before model building or CLI-style analysis. */
|
|
36
36
|
export { validateProfileCapture } from './schema.js';
|
|
37
|
-
export {
|
|
38
|
-
consumeToolPreviewProfile,
|
|
39
|
-
type ToolPreviewProfileManifest,
|
|
40
|
-
type ToolPreviewProfileRef,
|
|
41
|
-
type ToolPreviewProfileResult,
|
|
42
|
-
} from './tool-preview-profile.js';
|
|
43
37
|
export type {
|
|
44
38
|
ProfileAllocationReport,
|
|
45
39
|
ProfileCapture,
|
package/src/recorder.ts
CHANGED
|
@@ -34,14 +34,14 @@ const PHASE_RECORD_KIND = 1;
|
|
|
34
34
|
const SKIP_RECORD_KIND = 2;
|
|
35
35
|
|
|
36
36
|
interface RetainedRecordStore {
|
|
37
|
-
readonly kinds:
|
|
37
|
+
readonly kinds: number[];
|
|
38
38
|
readonly sources: ProfileSource[];
|
|
39
39
|
readonly phases: string[];
|
|
40
|
-
readonly frameIds:
|
|
40
|
+
readonly frameIds: number[];
|
|
41
41
|
readonly parentSources: Array<ProfileSource | undefined>;
|
|
42
42
|
readonly parentPhases: Array<string | undefined>;
|
|
43
|
-
readonly startMicros:
|
|
44
|
-
readonly endMicros:
|
|
43
|
+
readonly startMicros: number[];
|
|
44
|
+
readonly endMicros: number[];
|
|
45
45
|
readonly reasons: Array<string | undefined>;
|
|
46
46
|
count: number;
|
|
47
47
|
}
|
|
@@ -266,7 +266,6 @@ export function createRecorder(
|
|
|
266
266
|
): ProfilerResult<RecorderSession> {
|
|
267
267
|
const validLimits = validateRecorderLimits(limits);
|
|
268
268
|
if (!validLimits.ok) return validLimits;
|
|
269
|
-
const nowMicros = clock.nowMicros;
|
|
270
269
|
const state: RecorderState = {
|
|
271
270
|
limits,
|
|
272
271
|
phaseCatalog,
|
|
@@ -279,15 +278,15 @@ export function createRecorder(
|
|
|
279
278
|
render: new Set(phaseCatalog.render),
|
|
280
279
|
},
|
|
281
280
|
recordStore: {
|
|
282
|
-
kinds:
|
|
283
|
-
sources:
|
|
284
|
-
phases:
|
|
285
|
-
frameIds:
|
|
286
|
-
parentSources:
|
|
287
|
-
parentPhases:
|
|
288
|
-
startMicros:
|
|
289
|
-
endMicros:
|
|
290
|
-
reasons:
|
|
281
|
+
kinds: [],
|
|
282
|
+
sources: [],
|
|
283
|
+
phases: [],
|
|
284
|
+
frameIds: [],
|
|
285
|
+
parentSources: [],
|
|
286
|
+
parentPhases: [],
|
|
287
|
+
startMicros: [],
|
|
288
|
+
endMicros: [],
|
|
289
|
+
reasons: [],
|
|
291
290
|
count: 0,
|
|
292
291
|
},
|
|
293
292
|
captureId,
|
|
@@ -341,7 +340,7 @@ export function createRecorder(
|
|
|
341
340
|
state.openSources[index] = source;
|
|
342
341
|
state.openPhaseNames[index] = phase;
|
|
343
342
|
state.openFrameIds[index] = frameId;
|
|
344
|
-
state.openStartMicros[index] = nowMicros();
|
|
343
|
+
state.openStartMicros[index] = clock.nowMicros();
|
|
345
344
|
state.openDepth += 1;
|
|
346
345
|
return OK_VOID;
|
|
347
346
|
},
|
|
@@ -367,7 +366,7 @@ export function createRecorder(
|
|
|
367
366
|
const parentSource = parentIndex >= 0 ? state.openSources[parentIndex] : undefined;
|
|
368
367
|
const parentPhase = parentIndex >= 0 ? state.openPhaseNames[parentIndex] : undefined;
|
|
369
368
|
if (!state.overflow) {
|
|
370
|
-
const endMicros = Math.max(startMicros, nowMicros());
|
|
369
|
+
const endMicros = Math.max(startMicros, clock.nowMicros());
|
|
371
370
|
retainPhaseRecord(
|
|
372
371
|
state,
|
|
373
372
|
source,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-profile-detail-owner.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/browser-profile-detail-owner.test-d.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser-user-timing.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/browser-user-timing.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"comparison-side-owner.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/comparison-side-owner.test-d.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema-derived-union-owner.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schema-derived-union-owner.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { type ProfileCapture } from './index.js';
|
|
2
|
-
export interface ToolPreviewProfileRef {
|
|
3
|
-
readonly kind: 'profile-capture';
|
|
4
|
-
readonly digest: string;
|
|
5
|
-
readonly uri: string;
|
|
6
|
-
}
|
|
7
|
-
export interface ToolPreviewProfileManifest {
|
|
8
|
-
readonly runId: string;
|
|
9
|
-
readonly captureId: string;
|
|
10
|
-
readonly profileDigest: string;
|
|
11
|
-
}
|
|
12
|
-
export type ToolPreviewProfileResult = {
|
|
13
|
-
readonly ok: true;
|
|
14
|
-
readonly value: {
|
|
15
|
-
readonly runId: string;
|
|
16
|
-
readonly captureId: string;
|
|
17
|
-
readonly profile: ProfileCapture;
|
|
18
|
-
};
|
|
19
|
-
} | {
|
|
20
|
-
readonly ok: false;
|
|
21
|
-
readonly error: {
|
|
22
|
-
readonly code: string;
|
|
23
|
-
readonly expected: string;
|
|
24
|
-
readonly hint: string;
|
|
25
|
-
readonly detail: unknown;
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
export declare function consumeToolPreviewProfile(input: {
|
|
29
|
-
readonly manifest: ToolPreviewProfileManifest;
|
|
30
|
-
readonly profile: unknown;
|
|
31
|
-
readonly ref: ToolPreviewProfileRef;
|
|
32
|
-
}): ToolPreviewProfileResult;
|
|
33
|
-
//# sourceMappingURL=tool-preview-profile.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tool-preview-profile.d.ts","sourceRoot":"","sources":["../src/tool-preview-profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,wBAAwB,GAChC;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;KAClC,CAAC;CACH,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;KAC1B,CAAC;CACH,CAAC;AAEN,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAC/C,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,qBAAqB,CAAC;CACrC,GAAG,wBAAwB,CAiB3B"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
3
|
-
import type { ProfileDetail, RecorderSession } from '../index.js';
|
|
4
|
-
|
|
5
|
-
type ExpectedProfileDetail = 'owner' | 'passes' | 'nested';
|
|
6
|
-
const browserSource = readFileSync(new URL('../browser-user-timing.ts', import.meta.url), 'utf8');
|
|
7
|
-
const recorderSource = readFileSync(new URL('../recorder.ts', import.meta.url), 'utf8');
|
|
8
|
-
|
|
9
|
-
describe('browser ProfileDetail owner', () => {
|
|
10
|
-
it('preserves the public vocabulary and session detail projection', () => {
|
|
11
|
-
expectTypeOf<ProfileDetail>().toEqualTypeOf<ExpectedProfileDetail>();
|
|
12
|
-
expectTypeOf<RecorderSession['detail']>().toEqualTypeOf<ProfileDetail>();
|
|
13
|
-
expectTypeOf<ProfileDetail>().toEqualTypeOf<RecorderSession['detail']>();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('derives the private browser view from the recorder owner', () => {
|
|
17
|
-
expect(browserSource).toContain(
|
|
18
|
-
"import type { ProfileDetail, RecorderSession } from './recorder.js';",
|
|
19
|
-
);
|
|
20
|
-
expect(browserSource).toContain('type BrowserProfileDetail = ProfileDetail;');
|
|
21
|
-
expect(browserSource).not.toContain(
|
|
22
|
-
"type BrowserProfileDetail = 'owner' | 'passes' | 'nested';",
|
|
23
|
-
);
|
|
24
|
-
expect(recorderSource).toContain("export type ProfileDetail = 'owner' | 'passes' | 'nested';");
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { createUserTimingProfiler } from '../browser-user-timing.js';
|
|
3
|
-
import { runProfilerCli } from '../cli.js';
|
|
4
|
-
import { buildProfileModel } from '../model.js';
|
|
5
|
-
import { validateProfileCapture } from '../schema.js';
|
|
6
|
-
import type { ProfileCapture, ProfileResult } from '../types.js';
|
|
7
|
-
|
|
8
|
-
const DIAGNOSTICS_KEY = '__forgeaxFramePhaseDiagnostics';
|
|
9
|
-
const APP_PHASES = ['frame-total', 'world-update-primary'] as const;
|
|
10
|
-
const RENDER_PHASES = ['extract', 'record'] as const;
|
|
11
|
-
|
|
12
|
-
function expectOk<T, E>(result: ProfileResult<T, E>): T {
|
|
13
|
-
expect(result.ok).toBe(true);
|
|
14
|
-
if (!result.ok) throw new Error('expected a successful profiler result');
|
|
15
|
-
return result.value;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function enableDiagnostics(detail: 'owner' | 'passes' | 'nested' = 'nested'): void {
|
|
19
|
-
(globalThis as Record<string, unknown>)[DIAGNOSTICS_KEY] = { enabled: true, detail };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function registerCatalogs(
|
|
23
|
-
profiler: NonNullable<ReturnType<typeof createUserTimingProfiler>>,
|
|
24
|
-
): void {
|
|
25
|
-
expectOk(profiler.registerPhaseCatalog('app', APP_PHASES));
|
|
26
|
-
expectOk(profiler.registerPhaseCatalog('render', RENDER_PHASES));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function finishOneFrame(
|
|
30
|
-
profiler: NonNullable<ReturnType<typeof createUserTimingProfiler>>,
|
|
31
|
-
): ProfileCapture {
|
|
32
|
-
const session = expectOk(profiler.startCapture({ frameLimit: 2, eventLimit: 8 }));
|
|
33
|
-
expectOk(session.beginFrame(1));
|
|
34
|
-
expectOk(session.beginPhase({ source: 'app', phase: 'frame-total' }));
|
|
35
|
-
expectOk(session.endPhase());
|
|
36
|
-
expectOk(session.beginPhase({ source: 'render', phase: 'record' }));
|
|
37
|
-
expectOk(session.endPhase());
|
|
38
|
-
expectOk(session.endFrame());
|
|
39
|
-
return expectOk(session.finish());
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
Reflect.deleteProperty(globalThis, DIAGNOSTICS_KEY);
|
|
44
|
-
vi.unstubAllGlobals();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('browser User Timing profiler', () => {
|
|
48
|
-
it('emits a schema-valid default capture through model and CLI consumers', () => {
|
|
49
|
-
enableDiagnostics();
|
|
50
|
-
const profiler = createUserTimingProfiler();
|
|
51
|
-
expect(profiler).toBeDefined();
|
|
52
|
-
if (profiler === undefined) return;
|
|
53
|
-
registerCatalogs(profiler);
|
|
54
|
-
|
|
55
|
-
const capture = finishOneFrame(profiler);
|
|
56
|
-
|
|
57
|
-
expect(capture.captureId).toBe('capture-0001');
|
|
58
|
-
expect(capture.completeness).toEqual({
|
|
59
|
-
status: 'partial',
|
|
60
|
-
retainedEventCount: 0,
|
|
61
|
-
droppedEventCount: 0,
|
|
62
|
-
incompleteReason: 'user-timing-transport',
|
|
63
|
-
});
|
|
64
|
-
expect(validateProfileCapture(capture).ok).toBe(true);
|
|
65
|
-
|
|
66
|
-
const model = buildProfileModel(capture);
|
|
67
|
-
expect(model.ok).toBe(true);
|
|
68
|
-
if (!model.ok) return;
|
|
69
|
-
expect(model.value.summary.captureId).toBe('capture-0001');
|
|
70
|
-
|
|
71
|
-
const cli = runProfilerCli(['summary'], JSON.stringify(capture));
|
|
72
|
-
expect(cli.exitCode).toBe(0);
|
|
73
|
-
expect(JSON.parse(cli.stdout)).toMatchObject({ captureId: 'capture-0001' });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('preserves valid caller IDs and derives the default for invalid IDs', () => {
|
|
77
|
-
enableDiagnostics('owner');
|
|
78
|
-
const valid = createUserTimingProfiler({ captureId: 'capture-1234' });
|
|
79
|
-
expect(valid).toBeDefined();
|
|
80
|
-
if (valid === undefined) return;
|
|
81
|
-
expect(finishOneFrame(valid).captureId).toBe('capture-1234');
|
|
82
|
-
|
|
83
|
-
const invalid = createUserTimingProfiler({ captureId: 'host-session' });
|
|
84
|
-
expect(invalid).toBeDefined();
|
|
85
|
-
if (invalid === undefined) return;
|
|
86
|
-
const capture = finishOneFrame(invalid);
|
|
87
|
-
expect(capture.captureId).toBe('capture-0001');
|
|
88
|
-
expect(validateProfileCapture(capture).ok).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('keeps disabled creation inert and retains onPhaseEnd-only callbacks', () => {
|
|
92
|
-
const mark = vi.fn();
|
|
93
|
-
vi.stubGlobal('performance', { mark });
|
|
94
|
-
|
|
95
|
-
expect(createUserTimingProfiler()).toBeUndefined();
|
|
96
|
-
expect(mark).not.toHaveBeenCalled();
|
|
97
|
-
|
|
98
|
-
const onPhaseEnd = vi.fn();
|
|
99
|
-
const profiler = createUserTimingProfiler({ onPhaseEnd });
|
|
100
|
-
expect(profiler).toBeDefined();
|
|
101
|
-
if (profiler === undefined) return;
|
|
102
|
-
const capture = finishOneFrame(profiler);
|
|
103
|
-
|
|
104
|
-
expect(onPhaseEnd).toHaveBeenCalledWith({ source: 'app', phase: 'frame-total' });
|
|
105
|
-
expect(onPhaseEnd).toHaveBeenCalledWith({ source: 'render', phase: 'record' });
|
|
106
|
-
expect(mark).not.toHaveBeenCalled();
|
|
107
|
-
expect(validateProfileCapture(capture).ok).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('supports repeated start and finish with one partial artifact per session', () => {
|
|
111
|
-
enableDiagnostics('passes');
|
|
112
|
-
const profiler = createUserTimingProfiler();
|
|
113
|
-
expect(profiler).toBeDefined();
|
|
114
|
-
if (profiler === undefined) return;
|
|
115
|
-
|
|
116
|
-
const first = expectOk(profiler.startCapture({ frameLimit: 1, eventLimit: 1 }));
|
|
117
|
-
const firstCapture = expectOk(first.finish());
|
|
118
|
-
const second = expectOk(profiler.startCapture({ frameLimit: 1, eventLimit: 1 }));
|
|
119
|
-
const secondCapture = expectOk(second.finish());
|
|
120
|
-
|
|
121
|
-
expect(firstCapture.captureId).toBe('capture-0001');
|
|
122
|
-
expect(secondCapture.captureId).toBe('capture-0001');
|
|
123
|
-
expect(profiler.latestCapture()).toBe(secondCapture);
|
|
124
|
-
expect(validateProfileCapture(firstCapture).ok).toBe(true);
|
|
125
|
-
expect(validateProfileCapture(secondCapture).ok).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
3
|
-
import type { ProfileComparisonError } from '../index.js';
|
|
4
|
-
|
|
5
|
-
type ComparisonErrorSide = ProfileComparisonError['detail']['side'];
|
|
6
|
-
type ExpectedComparisonErrorSide = 'left' | 'right';
|
|
7
|
-
|
|
8
|
-
const compareSource = readFileSync(new URL('../compare.ts', import.meta.url), 'utf8');
|
|
9
|
-
|
|
10
|
-
describe('profile comparison side owner', () => {
|
|
11
|
-
it('keeps the public side vocabulary bilaterally exact', () => {
|
|
12
|
-
expectTypeOf<ComparisonErrorSide>().toEqualTypeOf<ExpectedComparisonErrorSide>();
|
|
13
|
-
expectTypeOf<ExpectedComparisonErrorSide>().toEqualTypeOf<ComparisonErrorSide>();
|
|
14
|
-
|
|
15
|
-
const acceptsSide = (side: ComparisonErrorSide): void => void side;
|
|
16
|
-
acceptsSide('left');
|
|
17
|
-
acceptsSide('right');
|
|
18
|
-
// @ts-expect-error Unknown sides remain outside the public comparison vocabulary.
|
|
19
|
-
acceptsSide('comparison-side-not-real');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('derives withSide from the public error detail owner', () => {
|
|
23
|
-
expect(compareSource).toContain("side: ProfileComparisonError['detail']['side'],");
|
|
24
|
-
expect(compareSource).not.toContain("side: 'left' | 'right'");
|
|
25
|
-
expect(compareSource).toContain("withSide('left', leftModel.error)");
|
|
26
|
-
expect(compareSource).toContain("withSide('right', rightModel.error)");
|
|
27
|
-
});
|
|
28
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
|
|
4
|
-
type Schema = { $defs: Record<string, unknown> };
|
|
5
|
-
|
|
6
|
-
function readSchema(): Schema {
|
|
7
|
-
return JSON.parse(
|
|
8
|
-
readFileSync(new URL('../../schema/profile-capture.schema.json', import.meta.url), 'utf8'),
|
|
9
|
-
) as Schema;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function literalUnion(values: readonly string[]): string {
|
|
13
|
-
return values.map((value) => `'${value}'`).join(' | ');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function readStringEnum(definition: unknown): readonly string[] {
|
|
17
|
-
return (definition as { enum: readonly string[] }).enum;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function readRecordKinds(schema: Schema): readonly string[] {
|
|
21
|
-
const record = schema.$defs.record as { oneOf: readonly { $ref: string }[] };
|
|
22
|
-
return record.oneOf.map(({ $ref }) => {
|
|
23
|
-
const definitionName = $ref.slice('#/$defs/'.length);
|
|
24
|
-
const definition = schema.$defs[definitionName] as {
|
|
25
|
-
properties: { kind: { const: string } };
|
|
26
|
-
};
|
|
27
|
-
return definition.properties.kind.const;
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('schema-derived profiler union owners', () => {
|
|
32
|
-
it('projects the three closed unions from schema memberships', () => {
|
|
33
|
-
const schema = readSchema();
|
|
34
|
-
const source = readFileSync(
|
|
35
|
-
new URL('../../scripts/generate-profile-types.mjs', import.meta.url),
|
|
36
|
-
'utf8',
|
|
37
|
-
);
|
|
38
|
-
const generated = readFileSync(
|
|
39
|
-
new URL('../generated/profile-capture.ts', import.meta.url),
|
|
40
|
-
'utf8',
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const sourceUnion = literalUnion(readStringEnum(schema.$defs.source));
|
|
44
|
-
const recordKindUnion = literalUnion(readRecordKinds(schema));
|
|
45
|
-
const completenessStatus = schema.$defs.completeness as {
|
|
46
|
-
properties: { status: { enum: readonly string[] } };
|
|
47
|
-
};
|
|
48
|
-
const completenessUnion = literalUnion(readStringEnum(completenessStatus.properties.status));
|
|
49
|
-
|
|
50
|
-
expect(generated).toContain(`export type ProfileSource = ${sourceUnion};`);
|
|
51
|
-
expect(generated).toContain(`export type ProfileRecordKind = ${recordKindUnion};`);
|
|
52
|
-
expect(generated).toContain(`export type ProfileCompletenessStatus = ${completenessUnion};`);
|
|
53
|
-
expect(source).toContain('schema.$defs.source.enum');
|
|
54
|
-
expect(source).toContain('schema.$defs.record.oneOf');
|
|
55
|
-
expect(source).toContain('schema.$defs.completeness.properties.status.enum');
|
|
56
|
-
});
|
|
57
|
-
});
|