@forgeax/engine-profiler 0.0.0-dev.8d955ade1c79

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 (103) 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__/cli-consumer.integration.test.d.ts +2 -0
  6. package/dist/__tests__/cli-consumer.integration.test.d.ts.map +1 -0
  7. package/dist/__tests__/cli-contract.test.d.ts +2 -0
  8. package/dist/__tests__/cli-contract.test.d.ts.map +1 -0
  9. package/dist/__tests__/cli-input-errors.test.d.ts +2 -0
  10. package/dist/__tests__/cli-input-errors.test.d.ts.map +1 -0
  11. package/dist/__tests__/cli-process.integration.test.d.ts +2 -0
  12. package/dist/__tests__/cli-process.integration.test.d.ts.map +1 -0
  13. package/dist/__tests__/compare.test.d.ts +2 -0
  14. package/dist/__tests__/compare.test.d.ts.map +1 -0
  15. package/dist/__tests__/consumer-smoke.integration.test.d.ts +2 -0
  16. package/dist/__tests__/consumer-smoke.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/generated-freshness.test.d.ts +2 -0
  18. package/dist/__tests__/generated-freshness.test.d.ts.map +1 -0
  19. package/dist/__tests__/model-determinism.test.d.ts +2 -0
  20. package/dist/__tests__/model-determinism.test.d.ts.map +1 -0
  21. package/dist/__tests__/model-query.test.d.ts +2 -0
  22. package/dist/__tests__/model-query.test.d.ts.map +1 -0
  23. package/dist/__tests__/overflow-long-tail.test.d.ts +2 -0
  24. package/dist/__tests__/overflow-long-tail.test.d.ts.map +1 -0
  25. package/dist/__tests__/profile-source-owner.test-d.d.ts +2 -0
  26. package/dist/__tests__/profile-source-owner.test-d.d.ts.map +1 -0
  27. package/dist/__tests__/profiler-error-owner.test-d.d.ts +2 -0
  28. package/dist/__tests__/profiler-error-owner.test-d.d.ts.map +1 -0
  29. package/dist/__tests__/published-entry.browser.test.d.ts +2 -0
  30. package/dist/__tests__/published-entry.browser.test.d.ts.map +1 -0
  31. package/dist/__tests__/recorder-contract.test.d.ts +2 -0
  32. package/dist/__tests__/recorder-contract.test.d.ts.map +1 -0
  33. package/dist/__tests__/schema-contract.test.d.ts +2 -0
  34. package/dist/__tests__/schema-contract.test.d.ts.map +1 -0
  35. package/dist/browser-user-timing.d.ts +24 -0
  36. package/dist/browser-user-timing.d.ts.map +1 -0
  37. package/dist/browser-user-timing.mjs +135 -0
  38. package/dist/browser-user-timing.mjs.map +1 -0
  39. package/dist/cli.d.ts +9 -0
  40. package/dist/cli.d.ts.map +1 -0
  41. package/dist/cli.mjs +7770 -0
  42. package/dist/cli.mjs.map +1 -0
  43. package/dist/clock.d.ts +5 -0
  44. package/dist/clock.d.ts.map +1 -0
  45. package/dist/compare.d.ts +42 -0
  46. package/dist/compare.d.ts.map +1 -0
  47. package/dist/errors.d.ts +64 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/generated/profile-capture.d.ts +44 -0
  50. package/dist/generated/profile-capture.d.ts.map +1 -0
  51. package/dist/index.d.ts +21 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.mjs +8074 -0
  54. package/dist/index.mjs.map +1 -0
  55. package/dist/model.d.ts +44 -0
  56. package/dist/model.d.ts.map +1 -0
  57. package/dist/profiler.d.ts +24 -0
  58. package/dist/profiler.d.ts.map +1 -0
  59. package/dist/recorder.d.ts +26 -0
  60. package/dist/recorder.d.ts.map +1 -0
  61. package/dist/schema.d.ts +13 -0
  62. package/dist/schema.d.ts.map +1 -0
  63. package/dist/types.d.ts +44 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/package.json +76 -0
  66. package/schema/profile-capture.schema.json +71 -0
  67. package/scripts/consume-smoke.mjs +17 -0
  68. package/scripts/generate-profile-types.mjs +66 -0
  69. package/src/__tests__/cli-consumer.integration.test.ts +113 -0
  70. package/src/__tests__/cli-contract.test.ts +115 -0
  71. package/src/__tests__/cli-input-errors.test.ts +171 -0
  72. package/src/__tests__/cli-process.integration.test.ts +127 -0
  73. package/src/__tests__/compare.test.ts +223 -0
  74. package/src/__tests__/consumer-smoke.integration.test.ts +37 -0
  75. package/src/__tests__/fixtures/cli/invalid-version.json +18 -0
  76. package/src/__tests__/fixtures/cli/partial-capture.json +28 -0
  77. package/src/__tests__/fixtures/cli/valid-capture.json +43 -0
  78. package/src/__tests__/fixtures/profile-capture/invalid-boundary.json +144 -0
  79. package/src/__tests__/fixtures/profile-capture/model-input.json +54 -0
  80. package/src/__tests__/fixtures/profile-capture/sink-failure.json +30 -0
  81. package/src/__tests__/fixtures/profile-capture/valid-complete.json +43 -0
  82. package/src/__tests__/fixtures/profile-capture/valid-overflow.json +45 -0
  83. package/src/__tests__/generated-freshness.test.ts +14 -0
  84. package/src/__tests__/model-determinism.test.ts +70 -0
  85. package/src/__tests__/model-query.test.ts +128 -0
  86. package/src/__tests__/overflow-long-tail.test.ts +131 -0
  87. package/src/__tests__/profile-source-owner.test-d.ts +45 -0
  88. package/src/__tests__/profiler-error-owner.test-d.ts +64 -0
  89. package/src/__tests__/published-entry.browser.test.ts +27 -0
  90. package/src/__tests__/recorder-contract.test.ts +163 -0
  91. package/src/__tests__/schema-contract.test.ts +42 -0
  92. package/src/browser-user-timing.ts +186 -0
  93. package/src/cli.ts +410 -0
  94. package/src/clock.ts +18 -0
  95. package/src/compare.ts +156 -0
  96. package/src/errors.ts +78 -0
  97. package/src/generated/profile-capture.ts +48 -0
  98. package/src/index.ts +48 -0
  99. package/src/model.ts +161 -0
  100. package/src/profiler.ts +201 -0
  101. package/src/recorder.ts +416 -0
  102. package/src/schema.ts +168 -0
  103. 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,CAsJjC"}
@@ -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,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.0.0-dev.8d955ade1c79",
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,66 @@
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
+
8
+ const output = `// Generated by scripts/generate-profile-types.mjs. Do not edit.
9
+ // Schema version: ${schema.properties.schemaVersion.const}
10
+
11
+ export type ProfileSource = 'app' | 'render';
12
+ export type ProfileRecordKind = 'phase' | 'skip';
13
+ export type ProfileCompletenessStatus = 'complete' | 'partial' | 'overflow';
14
+
15
+ export interface ProfilePhaseRecord {
16
+ readonly kind: 'phase';
17
+ readonly source: ProfileSource;
18
+ readonly frameId: number;
19
+ readonly phase: string;
20
+ readonly parentSource?: ProfileSource;
21
+ readonly parentPhase?: string;
22
+ readonly startMicros: number;
23
+ readonly endMicros: number;
24
+ readonly durationMicros: number;
25
+ }
26
+
27
+ export interface ProfileSkipRecord {
28
+ readonly kind: 'skip';
29
+ readonly source: ProfileSource;
30
+ readonly frameId: number;
31
+ readonly phase: string;
32
+ readonly reason: string;
33
+ }
34
+
35
+ export type ProfileRecord = ProfilePhaseRecord | ProfileSkipRecord;
36
+
37
+ export interface ProfileCompleteness {
38
+ readonly status: ProfileCompletenessStatus;
39
+ readonly retainedEventCount: number;
40
+ readonly droppedEventCount: number;
41
+ readonly incompleteReason?: string;
42
+ readonly firstAffectedFrameId?: number;
43
+ readonly lastAffectedFrameId?: number;
44
+ }
45
+
46
+ export interface ProfileCapture {
47
+ readonly schemaVersion: '1.0';
48
+ readonly captureId: string;
49
+ readonly timeUnit: 'microseconds';
50
+ readonly frameLimit: number;
51
+ readonly eventLimit: number;
52
+ readonly phaseCatalog: Readonly<{ app: readonly string[]; render: readonly string[] }>;
53
+ readonly records: readonly ProfileRecord[];
54
+ readonly completeness: ProfileCompleteness;
55
+ }
56
+ `;
57
+
58
+ if (process.argv.includes('--check')) {
59
+ const current = readFileSync(outputPath, 'utf8');
60
+ if (current !== output) {
61
+ process.stderr.write(`${outputPath.pathname} is stale\n`);
62
+ process.exitCode = 1;
63
+ }
64
+ } else {
65
+ writeFileSync(outputPath, output);
66
+ }
@@ -0,0 +1,113 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
7
+ const cliPath = fileURLToPath(new URL('../../dist/cli.mjs', import.meta.url));
8
+ const smokePath = fileURLToPath(new URL('../../scripts/consume-smoke.mjs', import.meta.url));
9
+ const fixtureRoot = new URL('./fixtures/cli/', import.meta.url);
10
+
11
+ function runCli(args: readonly string[], input = '') {
12
+ return spawnSync(process.execPath, [cliPath, ...args], {
13
+ cwd: packageRoot,
14
+ input,
15
+ encoding: 'utf8',
16
+ });
17
+ }
18
+
19
+ function readFixture(name: string): string {
20
+ return readFileSync(fileURLToPath(new URL(name, fixtureRoot)), 'utf8');
21
+ }
22
+
23
+ function runSummaryForFixture(name: string) {
24
+ const fixturePath = fileURLToPath(new URL(name, fixtureRoot));
25
+ return runCli(['summary', '--file', fixturePath]);
26
+ }
27
+
28
+ function parseObject(output: string): Record<string, unknown> {
29
+ return JSON.parse(output) as Record<string, unknown>;
30
+ }
31
+
32
+ describe('profiler CLI consumer smoke', () => {
33
+ it('preserves completeness states through the built public CLI', () => {
34
+ const complete = runSummaryForFixture('valid-capture.json');
35
+ const partial = runSummaryForFixture('partial-capture.json');
36
+ const overflow = runSummaryForFixture('../profile-capture/valid-overflow.json');
37
+ const empty = runCli(
38
+ ['summary'],
39
+ JSON.stringify({
40
+ schemaVersion: '1.0',
41
+ captureId: 'capture-0044',
42
+ timeUnit: 'microseconds',
43
+ frameLimit: 1,
44
+ eventLimit: 1,
45
+ phaseCatalog: { app: ['input'], render: ['submit'] },
46
+ records: [],
47
+ completeness: {
48
+ status: 'partial',
49
+ retainedEventCount: 0,
50
+ droppedEventCount: 0,
51
+ incompleteReason: 'stopped-before-frame',
52
+ },
53
+ }),
54
+ );
55
+
56
+ expect(complete.status).toBe(0);
57
+ expect(parseObject(complete.stdout).completeness).toMatchObject({ status: 'complete' });
58
+ expect(partial.status).toBe(0);
59
+ expect(parseObject(partial.stdout).completeness).toMatchObject({
60
+ status: 'partial',
61
+ incompleteReason: 'stopped-during-frame',
62
+ });
63
+ expect(overflow.status).toBe(0);
64
+ expect(parseObject(overflow.stdout).completeness).toMatchObject({
65
+ status: 'overflow',
66
+ droppedEventCount: 7,
67
+ });
68
+ expect(empty.status).toBe(0);
69
+ expect(parseObject(empty.stdout)).toMatchObject({
70
+ captureId: 'capture-0044',
71
+ frameCount: 0,
72
+ frameRange: null,
73
+ completeness: { status: 'partial' },
74
+ });
75
+ });
76
+
77
+ it('keeps incompatible artifacts on stderr and repeats deterministically', () => {
78
+ const input = readFixture('valid-capture.json');
79
+ const first = runCli(['summary'], input);
80
+ const second = runCli(['summary'], input);
81
+ const invalid = runSummaryForFixture('invalid-version.json');
82
+
83
+ expect(first.status).toBe(0);
84
+ expect(second.status).toBe(0);
85
+ expect(first.stdout).toBe(second.stdout);
86
+ expect(first.stderr).toBe('');
87
+ expect(invalid.status).not.toBe(0);
88
+ expect(invalid.stdout).toBe('');
89
+ expect(parseObject(invalid.stderr)).toEqual({
90
+ error: {
91
+ code: 'profile-artifact-incompatible',
92
+ expected: expect.any(String),
93
+ hint: expect.any(String),
94
+ detail: expect.any(Object),
95
+ },
96
+ });
97
+ });
98
+
99
+ it('runs the package consumer smoke through the public CLI entry', () => {
100
+ const result = spawnSync(process.execPath, [smokePath], {
101
+ cwd: packageRoot,
102
+ encoding: 'utf8',
103
+ });
104
+
105
+ expect(result.status).toBe(0);
106
+ expect(result.stderr).toBe('');
107
+ expect(parseObject(result.stdout)).toMatchObject({
108
+ query: 'summary',
109
+ captureId: 'capture-0010',
110
+ completeness: { status: 'overflow' },
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,115 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { runProfilerCli } from '../cli.js';
4
+
5
+ type CliResponse = Record<string, unknown>;
6
+
7
+ function readFixture(name: string): string {
8
+ return readFileSync(new URL(`./fixtures/profile-capture/${name}`, import.meta.url), 'utf8');
9
+ }
10
+
11
+ function readCliFixture(name: string): string {
12
+ return readFileSync(new URL(`./fixtures/cli/${name}`, import.meta.url), 'utf8');
13
+ }
14
+
15
+ function parseOutput(output: string): CliResponse {
16
+ return JSON.parse(output) as CliResponse;
17
+ }
18
+
19
+ function expectJsonObject(output: string): CliResponse {
20
+ const parsed = parseOutput(output);
21
+ expect(parsed).toEqual(expect.any(Object));
22
+ return parsed;
23
+ }
24
+
25
+ describe('profiler CLI structured output contract', () => {
26
+ it('prints one machine-readable summary object by default', () => {
27
+ const result = runProfilerCli([], readFixture('valid-complete.json'));
28
+ const output = expectJsonObject(result.stdout);
29
+
30
+ expect(result.exitCode).toBe(0);
31
+ expect(result.stderr).toBe('');
32
+ expect(output).toMatchObject({
33
+ query: 'summary',
34
+ schemaVersion: '1.0',
35
+ captureId: 'capture-0001',
36
+ frameRange: { first: 1, last: 2 },
37
+ completeness: { status: 'complete' },
38
+ });
39
+ expect(output.phases).toEqual(expect.any(Array));
40
+ });
41
+
42
+ it('returns a frame projection selected by frameId', () => {
43
+ const result = runProfilerCli(['frame', '--frame-id', '2'], readFixture('valid-overflow.json'));
44
+ const output = expectJsonObject(result.stdout);
45
+
46
+ expect(result.exitCode).toBe(0);
47
+ expect(result.stderr).toBe('');
48
+ expect(output).toMatchObject({
49
+ query: 'frame',
50
+ captureId: 'capture-0002',
51
+ frameId: 2,
52
+ completeness: {
53
+ status: 'overflow',
54
+ droppedEventCount: 7,
55
+ firstAffectedFrameId: 2,
56
+ lastAffectedFrameId: 4,
57
+ },
58
+ frame: null,
59
+ });
60
+ });
61
+
62
+ it('returns a phase projection selected by source and phase', () => {
63
+ const result = runProfilerCli(
64
+ ['phase', '--source', 'render', '--phase', 'submit'],
65
+ readFixture('valid-complete.json'),
66
+ );
67
+ const output = expectJsonObject(result.stdout);
68
+
69
+ expect(result.exitCode).toBe(0);
70
+ expect(result.stderr).toBe('');
71
+ expect(output).toMatchObject({
72
+ query: 'phase',
73
+ captureId: 'capture-0001',
74
+ source: 'render',
75
+ phase: 'submit',
76
+ phaseSummary: {
77
+ source: 'render',
78
+ phase: 'submit',
79
+ skipCount: 1,
80
+ },
81
+ });
82
+ });
83
+
84
+ it('accepts file input and preserves an empty capture status', () => {
85
+ const result = runProfilerCli(
86
+ [
87
+ 'summary',
88
+ '--file',
89
+ new URL('./fixtures/profile-capture/valid-complete.json', import.meta.url).pathname,
90
+ ],
91
+ '',
92
+ );
93
+ const output = expectJsonObject(result.stdout);
94
+
95
+ expect(result.exitCode).toBe(0);
96
+ expect(result.stderr).toBe('');
97
+ expect(output.completeness).toMatchObject({ status: 'complete' });
98
+ });
99
+
100
+ it('writes one structured error object for an incompatible artifact', () => {
101
+ const result = runProfilerCli(['summary'], readCliFixture('invalid-version.json'));
102
+ const error = expectJsonObject(result.stderr);
103
+
104
+ expect(result.exitCode).not.toBe(0);
105
+ expect(result.stdout).toBe('');
106
+ expect(error).toMatchObject({
107
+ error: {
108
+ code: 'profile-artifact-incompatible',
109
+ expected: expect.any(String),
110
+ hint: expect.any(String),
111
+ detail: expect.any(Object),
112
+ },
113
+ });
114
+ });
115
+ });