@forgeax/engine-profiler 0.1.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 (118) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +164 -0
  3. package/bin/forgeax-engine-profiler.mjs +5 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/__tests__/browser-profile-detail-owner.test-d.d.ts +2 -0
  6. package/dist/__tests__/browser-profile-detail-owner.test-d.d.ts.map +1 -0
  7. package/dist/__tests__/browser-user-timing.test.d.ts +2 -0
  8. package/dist/__tests__/browser-user-timing.test.d.ts.map +1 -0
  9. package/dist/__tests__/cli-consumer.integration.test.d.ts +2 -0
  10. package/dist/__tests__/cli-consumer.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/cli-contract.test.d.ts +2 -0
  12. package/dist/__tests__/cli-contract.test.d.ts.map +1 -0
  13. package/dist/__tests__/cli-input-errors.test.d.ts +2 -0
  14. package/dist/__tests__/cli-input-errors.test.d.ts.map +1 -0
  15. package/dist/__tests__/cli-process.integration.test.d.ts +2 -0
  16. package/dist/__tests__/cli-process.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/compare.test.d.ts +2 -0
  18. package/dist/__tests__/compare.test.d.ts.map +1 -0
  19. package/dist/__tests__/comparison-side-owner.test-d.d.ts +2 -0
  20. package/dist/__tests__/comparison-side-owner.test-d.d.ts.map +1 -0
  21. package/dist/__tests__/consumer-smoke.integration.test.d.ts +2 -0
  22. package/dist/__tests__/consumer-smoke.integration.test.d.ts.map +1 -0
  23. package/dist/__tests__/generated-freshness.test.d.ts +2 -0
  24. package/dist/__tests__/generated-freshness.test.d.ts.map +1 -0
  25. package/dist/__tests__/model-determinism.test.d.ts +2 -0
  26. package/dist/__tests__/model-determinism.test.d.ts.map +1 -0
  27. package/dist/__tests__/model-query.test.d.ts +2 -0
  28. package/dist/__tests__/model-query.test.d.ts.map +1 -0
  29. package/dist/__tests__/overflow-long-tail.test.d.ts +2 -0
  30. package/dist/__tests__/overflow-long-tail.test.d.ts.map +1 -0
  31. package/dist/__tests__/profile-source-owner.test-d.d.ts +2 -0
  32. package/dist/__tests__/profile-source-owner.test-d.d.ts.map +1 -0
  33. package/dist/__tests__/profiler-error-owner.test-d.d.ts +2 -0
  34. package/dist/__tests__/profiler-error-owner.test-d.d.ts.map +1 -0
  35. package/dist/__tests__/published-entry.browser.test.d.ts +2 -0
  36. package/dist/__tests__/published-entry.browser.test.d.ts.map +1 -0
  37. package/dist/__tests__/recorder-contract.test.d.ts +2 -0
  38. package/dist/__tests__/recorder-contract.test.d.ts.map +1 -0
  39. package/dist/__tests__/schema-contract.test.d.ts +2 -0
  40. package/dist/__tests__/schema-contract.test.d.ts.map +1 -0
  41. package/dist/__tests__/schema-derived-union-owner.test.d.ts +2 -0
  42. package/dist/__tests__/schema-derived-union-owner.test.d.ts.map +1 -0
  43. package/dist/browser-user-timing.d.ts +24 -0
  44. package/dist/browser-user-timing.d.ts.map +1 -0
  45. package/dist/browser-user-timing.mjs +140 -0
  46. package/dist/browser-user-timing.mjs.map +1 -0
  47. package/dist/cli.d.ts +21 -0
  48. package/dist/cli.d.ts.map +1 -0
  49. package/dist/cli.mjs +7783 -0
  50. package/dist/cli.mjs.map +1 -0
  51. package/dist/clock.d.ts +5 -0
  52. package/dist/clock.d.ts.map +1 -0
  53. package/dist/compare.d.ts +42 -0
  54. package/dist/compare.d.ts.map +1 -0
  55. package/dist/errors.d.ts +64 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/generated/profile-capture.d.ts +44 -0
  58. package/dist/generated/profile-capture.d.ts.map +1 -0
  59. package/dist/index.d.ts +22 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.mjs +8111 -0
  62. package/dist/index.mjs.map +1 -0
  63. package/dist/model.d.ts +44 -0
  64. package/dist/model.d.ts.map +1 -0
  65. package/dist/profiler.d.ts +24 -0
  66. package/dist/profiler.d.ts.map +1 -0
  67. package/dist/recorder.d.ts +26 -0
  68. package/dist/recorder.d.ts.map +1 -0
  69. package/dist/schema.d.ts +13 -0
  70. package/dist/schema.d.ts.map +1 -0
  71. package/dist/tool-preview-profile.d.ts +33 -0
  72. package/dist/tool-preview-profile.d.ts.map +1 -0
  73. package/dist/types.d.ts +44 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/package.json +76 -0
  76. package/schema/profile-capture.schema.json +71 -0
  77. package/scripts/consume-smoke.mjs +17 -0
  78. package/scripts/generate-profile-types.mjs +75 -0
  79. package/src/__tests__/browser-profile-detail-owner.test-d.ts +26 -0
  80. package/src/__tests__/browser-user-timing.test.ts +127 -0
  81. package/src/__tests__/cli-consumer.integration.test.ts +113 -0
  82. package/src/__tests__/cli-contract.test.ts +115 -0
  83. package/src/__tests__/cli-input-errors.test.ts +210 -0
  84. package/src/__tests__/cli-process.integration.test.ts +154 -0
  85. package/src/__tests__/compare.test.ts +223 -0
  86. package/src/__tests__/comparison-side-owner.test-d.ts +28 -0
  87. package/src/__tests__/consumer-smoke.integration.test.ts +37 -0
  88. package/src/__tests__/fixtures/cli/invalid-version.json +18 -0
  89. package/src/__tests__/fixtures/cli/partial-capture.json +28 -0
  90. package/src/__tests__/fixtures/cli/valid-capture.json +43 -0
  91. package/src/__tests__/fixtures/profile-capture/invalid-boundary.json +144 -0
  92. package/src/__tests__/fixtures/profile-capture/model-input.json +54 -0
  93. package/src/__tests__/fixtures/profile-capture/sink-failure.json +30 -0
  94. package/src/__tests__/fixtures/profile-capture/valid-complete.json +43 -0
  95. package/src/__tests__/fixtures/profile-capture/valid-overflow.json +45 -0
  96. package/src/__tests__/generated-freshness.test.ts +14 -0
  97. package/src/__tests__/model-determinism.test.ts +70 -0
  98. package/src/__tests__/model-query.test.ts +128 -0
  99. package/src/__tests__/overflow-long-tail.test.ts +131 -0
  100. package/src/__tests__/profile-source-owner.test-d.ts +45 -0
  101. package/src/__tests__/profiler-error-owner.test-d.ts +64 -0
  102. package/src/__tests__/published-entry.browser.test.ts +27 -0
  103. package/src/__tests__/recorder-contract.test.ts +163 -0
  104. package/src/__tests__/schema-contract.test.ts +42 -0
  105. package/src/__tests__/schema-derived-union-owner.test.ts +57 -0
  106. package/src/browser-user-timing.ts +194 -0
  107. package/src/cli.ts +425 -0
  108. package/src/clock.ts +18 -0
  109. package/src/compare.ts +159 -0
  110. package/src/errors.ts +78 -0
  111. package/src/generated/profile-capture.ts +48 -0
  112. package/src/index.ts +54 -0
  113. package/src/model.ts +161 -0
  114. package/src/profiler.ts +201 -0
  115. package/src/recorder.ts +417 -0
  116. package/src/schema.ts +168 -0
  117. package/src/tool-preview-profile.ts +67 -0
  118. package/src/types.ts +56 -0
@@ -0,0 +1,44 @@
1
+ import { type ProfileArtifactError } from './schema.js';
2
+ import type { ProfileCapture, ProfilePhaseRecord, ProfileRecord, ProfileResult } from './types.js';
3
+ export interface ProfileFrameModel {
4
+ readonly frameId: number;
5
+ readonly recordCount: number;
6
+ readonly phaseCount: number;
7
+ readonly skipCount: number;
8
+ readonly durationMicros: number;
9
+ readonly records: readonly ProfileRecord[];
10
+ }
11
+ export interface ProfilePhaseModel {
12
+ readonly source: ProfilePhaseRecord['source'];
13
+ readonly phase: string;
14
+ readonly parentSource?: ProfilePhaseRecord['source'];
15
+ readonly parentPhase?: string;
16
+ readonly count: number;
17
+ readonly skipCount: number;
18
+ readonly p95DurationMicros: number | null;
19
+ }
20
+ export interface ProfileSummaryModel {
21
+ readonly schemaVersion: ProfileCapture['schemaVersion'];
22
+ readonly captureId: string;
23
+ readonly timeUnit: ProfileCapture['timeUnit'];
24
+ readonly frameLimit: number;
25
+ readonly eventLimit: number;
26
+ readonly completeness: ProfileCapture['completeness'];
27
+ readonly frameRange: {
28
+ readonly first: number;
29
+ readonly last: number;
30
+ } | null;
31
+ readonly frameCount: number;
32
+ readonly recordCount: number;
33
+ readonly phaseCount: number;
34
+ readonly skipCount: number;
35
+ readonly p95DurationMicros: number | null;
36
+ }
37
+ export interface ProfileModel {
38
+ readonly summary: ProfileSummaryModel;
39
+ readonly completeness: ProfileCapture['completeness'];
40
+ readonly frames: readonly ProfileFrameModel[];
41
+ readonly phases: readonly ProfilePhaseModel[];
42
+ }
43
+ export declare function buildProfileModel(value: unknown): ProfileResult<ProfileModel, ProfileArtifactError>;
44
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,oBAAoB,EAA0B,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEnG,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,aAAa,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;IACxD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;IACtD,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC/C;AAgFD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,GACb,aAAa,CAAC,YAAY,EAAE,oBAAoB,CAAC,CAoCnD"}
@@ -0,0 +1,24 @@
1
+ import { type ProfileClock } from './clock.js';
2
+ import type { ProfilerError, ProfilerResult } from './errors.js';
3
+ import type { RecorderPhaseCatalog } from './recorder.js';
4
+ import { type RecorderLimits, type RecorderSession } from './recorder.js';
5
+ import type { ProfileAllocationReport, ProfileCapture, ProfileSink } from './types.js';
6
+ export interface ProfilerOptions {
7
+ readonly clock?: ProfileClock;
8
+ readonly sink?: ProfileSink;
9
+ readonly enabled?: boolean;
10
+ readonly phaseCatalog?: RecorderPhaseCatalog;
11
+ readonly allocationReport?: ProfileAllocationReport;
12
+ }
13
+ export type ProfilerCatalogSource = keyof RecorderPhaseCatalog;
14
+ export interface Profiler {
15
+ registerPhaseCatalog(source: ProfilerCatalogSource, phases: readonly string[]): ProfilerResult<() => void>;
16
+ startCapture(limits: RecorderLimits): ProfilerResult<RecorderSession>;
17
+ activeCaptureId(): string | undefined;
18
+ activeSession(): RecorderSession | undefined;
19
+ latestCapture(): ProfileCapture | undefined;
20
+ readonly phaseCatalog: RecorderPhaseCatalog;
21
+ }
22
+ export declare function createProfiler(options?: ProfilerOptions): Profiler;
23
+ export type { ProfileCapture, ProfilerError };
24
+ //# sourceMappingURL=profiler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiler.d.ts","sourceRoot":"","sources":["../src/profiler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAkB,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1F,OAAO,KAAK,EAAE,uBAAuB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEvF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,oBAAoB,CAAC;IAC7C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;CACrD;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,oBAAoB,CAAC;AAE/D,MAAM,WAAW,QAAQ;IACvB,oBAAoB,CAClB,MAAM,EAAE,qBAAqB,EAC7B,MAAM,EAAE,SAAS,MAAM,EAAE,GACxB,cAAc,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAAC,eAAe,CAAC,CAAC;IACtE,eAAe,IAAI,MAAM,GAAG,SAAS,CAAC;IACtC,aAAa,IAAI,eAAe,GAAG,SAAS,CAAC;IAC7C,aAAa,IAAI,cAAc,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;CAC7C;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,eAAoB,GAAG,QAAQ,CAsGtE;AAsED,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { ProfileClock } from './clock.js';
2
+ import { type ProfilerResult } from './errors.js';
3
+ import type { ProfileCapture, ProfilePhaseStart, ProfileSkipInput, ProfileSource } from './types.js';
4
+ export interface RecorderLimits {
5
+ readonly frameLimit: number;
6
+ readonly eventLimit: number;
7
+ readonly detail?: ProfileDetail;
8
+ }
9
+ export type ProfileDetail = 'owner' | 'passes' | 'nested';
10
+ export type RecorderPhaseCatalog = Readonly<Record<ProfileSource, readonly string[]>>;
11
+ export interface RecorderSession {
12
+ readonly captureId: string;
13
+ readonly detail: ProfileDetail;
14
+ beginFrame(frameId: number): ProfilerResult<void>;
15
+ beginPhase(input: ProfilePhaseStart): ProfilerResult<void>;
16
+ beginPhase(source: ProfileSource, phase: string): ProfilerResult<void>;
17
+ endPhase(): ProfilerResult<void>;
18
+ recordSkip(input: ProfileSkipInput): ProfilerResult<void>;
19
+ endFrame(): ProfilerResult<void>;
20
+ finish(): ProfilerResult<ProfileCapture>;
21
+ }
22
+ export declare function validateRecorderLimits(limits: RecorderLimits): ProfilerResult<void>;
23
+ export declare function createRecorder(captureId: string, limits: RecorderLimits, clock: ProfileClock, phaseCatalog: RecorderPhaseCatalog, allocationReport?: {
24
+ profilerEventObjectAllocations: number;
25
+ }): ProfilerResult<RecorderSession>;
26
+ //# sourceMappingURL=recorder.d.ts.map
@@ -0,0 +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,CAuJjC"}
@@ -0,0 +1,13 @@
1
+ import type { ProfileCapture } from './generated/profile-capture.js';
2
+ import type { ProfileResult } from './types.js';
3
+ export type ProfileArtifactError = {
4
+ readonly code: 'profile-artifact-invalid' | 'profile-artifact-incompatible';
5
+ readonly expected: string;
6
+ readonly hint: string;
7
+ readonly detail: {
8
+ readonly path: string;
9
+ readonly message: string;
10
+ };
11
+ };
12
+ export declare function validateProfileCapture(value: unknown): ProfileResult<ProfileCapture, ProfileArtifactError>;
13
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,gCAAgC,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,IAAI,EAAE,0BAA0B,GAAG,+BAA+B,CAAC;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACtE,CAAC;AAsIF,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,GACb,aAAa,CAAC,cAAc,EAAE,oBAAoB,CAAC,CAkBrD"}
@@ -0,0 +1,33 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,44 @@
1
+ import type { ProfileCapture, ProfileCompleteness, ProfilePhaseRecord, ProfileRecord, ProfileSkipRecord, ProfileSource } from './generated/profile-capture.js';
2
+ export type { ProfileCapture, ProfileCompleteness, ProfilePhaseRecord, ProfileRecord, ProfileSkipRecord, ProfileSource, };
3
+ export type ProfileResult<T, E> = {
4
+ readonly ok: true;
5
+ readonly value: T;
6
+ } | {
7
+ readonly ok: false;
8
+ readonly error: E;
9
+ };
10
+ export type ProfileSinkFailure = {
11
+ readonly code: string;
12
+ readonly expected?: string;
13
+ readonly hint?: string;
14
+ readonly detail?: {
15
+ readonly message?: string;
16
+ };
17
+ };
18
+ export type ProfileSinkWriteResult = {
19
+ readonly ok: true;
20
+ readonly value?: undefined;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly error: ProfileSinkFailure;
24
+ };
25
+ export interface ProfileSink {
26
+ write(capture: ProfileCapture): void | ProfileSinkWriteResult;
27
+ }
28
+ export interface ProfileAllocationReport {
29
+ profilerEventObjectAllocations: number;
30
+ }
31
+ export interface ProfileFrameToken {
32
+ readonly captureId: string;
33
+ readonly frameId: number;
34
+ }
35
+ export interface ProfilePhaseStart {
36
+ readonly source: ProfileSource;
37
+ readonly phase: string;
38
+ }
39
+ export interface ProfileSkipInput {
40
+ readonly source: ProfileSource;
41
+ readonly phase: string;
42
+ readonly reason: string;
43
+ }
44
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,aAAa,EACd,MAAM,gCAAgC,CAAC;AAExC,YAAY,EACV,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,aAAa,GACd,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,CAAC,EAAE,CAAC,IAC1B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACxC;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GACjD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAE/D,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,sBAAsB,CAAC;CAC/D;AAED,MAAM,WAAW,uBAAuB;IACtC,8BAA8B,EAAE,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB"}
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@forgeax/engine-profiler",
3
+ "version": "0.1.2",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "sideEffects": false,
8
+ "description": "CPU profile capture, bounded recording, and deterministic offline analysis for forgeax-engine.",
9
+ "bin": {
10
+ "forgeax-engine-profiler": "./bin/forgeax-engine-profiler.mjs"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs"
16
+ },
17
+ "./cli": {
18
+ "types": "./dist/cli.d.ts",
19
+ "import": "./dist/cli.mjs"
20
+ },
21
+ "./browser-user-timing": {
22
+ "types": "./dist/browser-user-timing.d.ts",
23
+ "import": "./dist/browser-user-timing.mjs"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "main": "./dist/index.mjs",
28
+ "types": "./dist/index.d.ts",
29
+ "files": [
30
+ "bin",
31
+ "dist",
32
+ "schema",
33
+ "src",
34
+ "scripts",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "dependencies": {
39
+ "ajv": "^8.17.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.14.0"
43
+ },
44
+ "forgeax": {
45
+ "metrics": {
46
+ "bundle-size": {
47
+ "enabled": true,
48
+ "path": "dist/index.mjs",
49
+ "compression": "gzip"
50
+ },
51
+ "fps": {
52
+ "enabled": false,
53
+ "reason": "library package without a canvas frame loop"
54
+ },
55
+ "bench": {
56
+ "enabled": false,
57
+ "reason": "performance gate runs through the package test:perf script"
58
+ },
59
+ "gate": {
60
+ "enabled": true,
61
+ "command": "pnpm --filter @forgeax/engine-profiler test:perf"
62
+ },
63
+ "spike-report": {
64
+ "enabled": false,
65
+ "reason": "not a spike package"
66
+ }
67
+ }
68
+ },
69
+ "scripts": {
70
+ "build": "tsup",
71
+ "cli": "node bin/forgeax-engine-profiler.mjs",
72
+ "test": "vitest run",
73
+ "smoke:consumer": "node scripts/consume-smoke.mjs",
74
+ "test:perf": "node ../../scripts/bench/profiler-overhead.mjs"
75
+ }
76
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://forgeax.dev/schemas/profile-capture-1.0.json",
4
+ "title": "ForgeaX ProfileCapture",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "captureId", "timeUnit", "frameLimit", "eventLimit", "phaseCatalog", "records", "completeness"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "1.0" },
10
+ "captureId": { "type": "string", "pattern": "^capture-[0-9]{4,}$" },
11
+ "timeUnit": { "const": "microseconds" },
12
+ "frameLimit": { "$ref": "#/$defs/positiveSafeInteger" },
13
+ "eventLimit": { "$ref": "#/$defs/positiveSafeInteger" },
14
+ "phaseCatalog": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["app", "render"],
18
+ "properties": { "app": { "$ref": "#/$defs/phaseNames" }, "render": { "$ref": "#/$defs/phaseNames" } }
19
+ },
20
+ "records": { "type": "array", "items": { "$ref": "#/$defs/record" } },
21
+ "completeness": { "$ref": "#/$defs/completeness" }
22
+ },
23
+ "$defs": {
24
+ "positiveSafeInteger": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
25
+ "nonNegativeSafeInteger": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
26
+ "phaseNames": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
27
+ "source": { "enum": ["app", "render"] },
28
+ "phaseRecord": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["kind", "source", "frameId", "phase", "startMicros", "endMicros", "durationMicros"],
32
+ "properties": {
33
+ "kind": { "const": "phase" },
34
+ "source": { "$ref": "#/$defs/source" },
35
+ "frameId": { "$ref": "#/$defs/positiveSafeInteger" },
36
+ "phase": { "type": "string", "minLength": 1 },
37
+ "parentSource": { "$ref": "#/$defs/source" },
38
+ "parentPhase": { "type": "string", "minLength": 1 },
39
+ "startMicros": { "$ref": "#/$defs/nonNegativeSafeInteger" },
40
+ "endMicros": { "$ref": "#/$defs/nonNegativeSafeInteger" },
41
+ "durationMicros": { "$ref": "#/$defs/nonNegativeSafeInteger" }
42
+ }
43
+ },
44
+ "skipRecord": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "required": ["kind", "source", "frameId", "phase", "reason"],
48
+ "properties": {
49
+ "kind": { "const": "skip" },
50
+ "source": { "$ref": "#/$defs/source" },
51
+ "frameId": { "$ref": "#/$defs/positiveSafeInteger" },
52
+ "phase": { "type": "string", "minLength": 1 },
53
+ "reason": { "type": "string", "minLength": 1 }
54
+ }
55
+ },
56
+ "record": { "oneOf": [{ "$ref": "#/$defs/phaseRecord" }, { "$ref": "#/$defs/skipRecord" }] },
57
+ "completeness": {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "required": ["status", "retainedEventCount", "droppedEventCount"],
61
+ "properties": {
62
+ "status": { "enum": ["complete", "partial", "overflow"] },
63
+ "retainedEventCount": { "$ref": "#/$defs/nonNegativeSafeInteger" },
64
+ "droppedEventCount": { "$ref": "#/$defs/nonNegativeSafeInteger" },
65
+ "incompleteReason": { "type": "string", "minLength": 1 },
66
+ "firstAffectedFrameId": { "$ref": "#/$defs/positiveSafeInteger" },
67
+ "lastAffectedFrameId": { "$ref": "#/$defs/positiveSafeInteger" }
68
+ }
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,17 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const cliPath = fileURLToPath(new URL('../dist/cli.mjs', import.meta.url));
5
+ const fixturePath = fileURLToPath(
6
+ new URL('../src/__tests__/fixtures/profile-capture/model-input.json', import.meta.url),
7
+ );
8
+ const result = spawnSync(process.execPath, [cliPath, 'summary', '--file', fixturePath], {
9
+ encoding: 'utf8',
10
+ });
11
+
12
+ if (result.status !== 0) {
13
+ process.stderr.write(result.stderr);
14
+ process.exitCode = result.status ?? 1;
15
+ } else {
16
+ process.stdout.write(result.stdout);
17
+ }
@@ -0,0 +1,75 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+
3
+ const packageRoot = new URL('../', import.meta.url);
4
+ const schemaPath = new URL('./schema/profile-capture.schema.json', packageRoot);
5
+ const outputPath = new URL('./src/generated/profile-capture.ts', packageRoot);
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
+
17
+ const output = `// Generated by scripts/generate-profile-types.mjs. Do not edit.
18
+ // Schema version: ${schema.properties.schemaVersion.const}
19
+
20
+ export type ProfileSource = ${profileSource};
21
+ export type ProfileRecordKind = ${profileRecordKind};
22
+ export type ProfileCompletenessStatus = ${profileCompletenessStatus};
23
+
24
+ export interface ProfilePhaseRecord {
25
+ readonly kind: 'phase';
26
+ readonly source: ProfileSource;
27
+ readonly frameId: number;
28
+ readonly phase: string;
29
+ readonly parentSource?: ProfileSource;
30
+ readonly parentPhase?: string;
31
+ readonly startMicros: number;
32
+ readonly endMicros: number;
33
+ readonly durationMicros: number;
34
+ }
35
+
36
+ export interface ProfileSkipRecord {
37
+ readonly kind: 'skip';
38
+ readonly source: ProfileSource;
39
+ readonly frameId: number;
40
+ readonly phase: string;
41
+ readonly reason: string;
42
+ }
43
+
44
+ export type ProfileRecord = ProfilePhaseRecord | ProfileSkipRecord;
45
+
46
+ export interface ProfileCompleteness {
47
+ readonly status: ProfileCompletenessStatus;
48
+ readonly retainedEventCount: number;
49
+ readonly droppedEventCount: number;
50
+ readonly incompleteReason?: string;
51
+ readonly firstAffectedFrameId?: number;
52
+ readonly lastAffectedFrameId?: number;
53
+ }
54
+
55
+ export interface ProfileCapture {
56
+ readonly schemaVersion: '1.0';
57
+ readonly captureId: string;
58
+ readonly timeUnit: 'microseconds';
59
+ readonly frameLimit: number;
60
+ readonly eventLimit: number;
61
+ readonly phaseCatalog: Readonly<{ app: readonly string[]; render: readonly string[] }>;
62
+ readonly records: readonly ProfileRecord[];
63
+ readonly completeness: ProfileCompleteness;
64
+ }
65
+ `;
66
+
67
+ if (process.argv.includes('--check')) {
68
+ const current = readFileSync(outputPath, 'utf8');
69
+ if (current !== output) {
70
+ process.stderr.write(`${outputPath.pathname} is stale\n`);
71
+ process.exitCode = 1;
72
+ }
73
+ } else {
74
+ writeFileSync(outputPath, output);
75
+ }
@@ -0,0 +1,26 @@
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
+ });
@@ -0,0 +1,127 @@
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
+ });