@playfast/reform-profiler 1.0.2 → 1.1.0

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/package.json CHANGED
@@ -1,25 +1,28 @@
1
1
  {
2
2
  "name": "@playfast/reform-profiler",
3
- "playbook": "./playbook",
4
- "version": "1.0.2",
5
- "type": "module",
3
+ "version": "1.1.0",
6
4
  "description": "Performance profiler for reform scenes — a recording Instrumentation, Chrome DevTools trace export, terminal reports, and a React overlay. Dev builds get the real recorder; prod builds resolve to no-op stubs.",
7
5
  "keywords": [
8
- "reform",
6
+ "devtools",
9
7
  "effect",
10
- "profiler",
11
8
  "performance",
12
- "devtools"
9
+ "profiler",
10
+ "reform"
13
11
  ],
12
+ "bugs": {
13
+ "url": "https://github.com/playfast/reform/issues"
14
+ },
14
15
  "license": "MIT",
15
16
  "repository": {
16
17
  "type": "git",
17
18
  "url": "https://github.com/playfast/reform.git",
18
19
  "directory": "packages/reform-profiler"
19
20
  },
20
- "bugs": {
21
- "url": "https://github.com/playfast/reform/issues"
22
- },
21
+ "files": [
22
+ "src",
23
+ "README.md"
24
+ ],
25
+ "type": "module",
23
26
  "sideEffects": false,
24
27
  "exports": {
25
28
  "./package.json": "./package.json",
@@ -33,10 +36,9 @@
33
36
  },
34
37
  "./recorder": "./src/index.ts"
35
38
  },
36
- "files": [
37
- "src",
38
- "README.md"
39
- ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
40
42
  "scripts": {
41
43
  "clean": "rm -rf dist .tsbuildinfo",
42
44
  "check": "tsc --noEmit",
@@ -48,8 +50,8 @@
48
50
  "lint:fix": "oxlint --fix src"
49
51
  },
50
52
  "peerDependencies": {
51
- "effect": "*",
52
53
  "@playfast/reform": "*",
54
+ "effect": "*",
53
55
  "react": "^19.0.0"
54
56
  },
55
57
  "peerDependenciesMeta": {
@@ -57,7 +59,5 @@
57
59
  "optional": true
58
60
  }
59
61
  },
60
- "publishConfig": {
61
- "access": "public"
62
- }
62
+ "playbook": "./playbook"
63
63
  }
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ export {
7
7
  type ProfileSnapshot,
8
8
  type ProfileStat,
9
9
  } from './recorder'
10
- export { type ProfiledScene, profileScene } from './profileScene'
10
+ export { type CapturedProfiledScene, type ProfiledScene, profileScene } from './profileScene'
11
11
  export { type ChromeTrace, type ChromeTraceEvent, toChromeTrace, traceJson } from './trace'
12
12
  export { formatReport } from './report'
13
13
  export {
package/src/noop.ts CHANGED
@@ -1,10 +1,7 @@
1
- import { noopInstrumentation, type Scene, type UiContract } from '@playfast/reform'
2
- import type { ProfiledScene } from './profileScene'
3
- import type {
4
- ProfilerHandle,
5
- ProfilerOptionsExternalApi,
6
- ProfileSnapshot,
7
- } from './recorder'
1
+ import { type CapturedScene, type Scene, type UiContract } from '@playfast/reform'
2
+ import { noopInstrumentation } from '@playfast/reform/internal'
3
+ import type { CapturedProfiledScene, ProfiledScene } from './profileScene'
4
+ import type { ProfilerHandle, ProfilerOptionsExternalApi, ProfileSnapshot } from './recorder'
8
5
  import type { ProfilerRegistration } from './registry'
9
6
  import type { ChromeTrace } from './trace'
10
7
 
@@ -18,7 +15,7 @@ export type {
18
15
  ProfileSnapshot,
19
16
  ProfileStat,
20
17
  } from './recorder'
21
- export type { ProfiledScene } from './profileScene'
18
+ export type { CapturedProfiledScene, ProfiledScene } from './profileScene'
22
19
  export type { ChromeTrace, ChromeTraceEvent } from './trace'
23
20
  export type { ProfilerRegistration } from './registry'
24
21
 
@@ -47,10 +44,45 @@ export const noopProfiler: ProfilerHandle = {
47
44
  export const makeProfiler = (_options: ProfilerOptionsExternalApi = {}): ProfilerHandle =>
48
45
  noopProfiler
49
46
 
50
- export const profileScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
47
+ const profileCapturedScene = <
48
+ C extends UiContract,
49
+ S extends ReadonlyArray<unknown>,
50
+ Services,
51
+ P,
52
+ N extends string,
53
+ Identity,
54
+ >(
55
+ base: CapturedScene<C, S, Services, P, N, Identity>,
56
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity> => ({
57
+ scene: base,
58
+ profiler: noopProfiler,
59
+ })
60
+
61
+ export function profileScene<
62
+ C extends UiContract,
63
+ S extends ReadonlyArray<unknown>,
64
+ Services,
65
+ P,
66
+ N extends string,
67
+ Identity,
68
+ >(
69
+ base: CapturedScene<C, S, Services, P, N, Identity>,
70
+ _options?: ProfilerOptionsExternalApi,
71
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity>
72
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
73
+ base: Scene<C, S>,
74
+ _options?: ProfilerOptionsExternalApi,
75
+ ): ProfiledScene<C, S>
76
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
51
77
  base: Scene<C, S>,
52
78
  _options: ProfilerOptionsExternalApi = {},
53
- ): ProfiledScene<C, S> => ({ scene: base, profiler: noopProfiler })
79
+ ): ProfiledScene<C, S> {
80
+ return base.capture<ProfiledScene<C, S>>(
81
+ <Services, P, N extends string, Identity>(
82
+ exactScene: CapturedScene<C, S, Services, P, N, Identity>,
83
+ ): ProfiledScene<C, S> => profileCapturedScene(exactScene),
84
+ )
85
+ }
54
86
 
55
87
  const emptyTrace: ChromeTrace = { traceEvents: [], displayTimeUnit: 'ms' }
56
88
 
@@ -61,11 +93,15 @@ export const traceJson = (_profiler: ProfilerHandle): string =>
61
93
 
62
94
  export const formatReport = (_profiler: ProfilerHandle): string => ''
63
95
 
64
- export const registerProfiler = (_registration: ProfilerRegistration): (() => void) => () => {}
96
+ export const registerProfiler =
97
+ (_registration: ProfilerRegistration): (() => void) =>
98
+ () => {}
65
99
 
66
100
  export const listProfilers = (): ReadonlyArray<ProfilerRegistration> => []
67
101
 
68
- export const subscribeProfilers = (_listener: () => void): (() => void) => () => {}
102
+ export const subscribeProfilers =
103
+ (_listener: () => void): (() => void) =>
104
+ () => {}
69
105
 
70
106
  export const profilersVersion = (): number => 0
71
107
 
@@ -1,5 +1,6 @@
1
1
  import { Effect, Layer } from 'effect'
2
- import { profiledScene, type Scene, type UiContract } from '@playfast/reform'
2
+ import { type CapturedScene, profiledScene, type Scene, type UiContract } from '@playfast/reform'
3
+ import { type AnySceneCapture, type SceneCapture } from '@playfast/reform/internal'
3
4
  import { makeProfiler, type ProfilerHandle, type ProfilerOptionsExternalApi } from './recorder'
4
5
  import { registerProfiler } from './registry'
5
6
 
@@ -11,29 +12,94 @@ export interface ProfiledScene<
11
12
  readonly profiler: ProfilerHandle
12
13
  }
13
14
 
14
- export const profileScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
15
+ export interface CapturedProfiledScene<
16
+ C extends UiContract,
17
+ S extends ReadonlyArray<unknown>,
18
+ Services,
19
+ P,
20
+ N extends string,
21
+ Identity,
22
+ > extends ProfiledScene<C, S> {
23
+ readonly scene: CapturedScene<C, S, Services, P, N, Identity>
24
+ }
25
+
26
+ const makeLifecycle = (profiler: ProfilerHandle) =>
27
+ Layer.scopedDiscard(
28
+ Effect.acquireRelease(
29
+ Effect.sync(() => registerProfiler({ label: profiler.label, handle: profiler })),
30
+ (unregister) => Effect.sync(unregister),
31
+ ),
32
+ )
33
+
34
+ const withLifecycle = <
35
+ C extends UiContract,
36
+ S extends ReadonlyArray<unknown>,
37
+ Services,
38
+ P,
39
+ N extends string,
40
+ Identity,
41
+ >(
42
+ base: CapturedScene<C, S, Services, P, N, Identity>,
43
+ lifecycle: ReturnType<typeof makeLifecycle>,
44
+ ): CapturedScene<C, S, Services, P, N, Identity> => {
45
+ const captured: CapturedScene<C, S, Services, P, N, Identity> = {
46
+ kind: 'Scene',
47
+ composition: base.composition,
48
+ // Lifecycle on the first provide layer tracks the runtime's build/dispose scope.
49
+ provide: base.provide.map((layer, index) =>
50
+ index === 0 ? Layer.merge(layer, lifecycle) : layer,
51
+ ),
52
+ ...(base.boot === undefined ? {} : { boot: base.boot }),
53
+ ...(base.instrumentation === undefined ? {} : { instrumentation: base.instrumentation }),
54
+ capture: <Result>(capture: SceneCapture<C, S, Result>): Result => capture(captured),
55
+ captureAny: <Result>(capture: AnySceneCapture<Result>): Result => capture(captured),
56
+ }
57
+ return captured
58
+ }
59
+
60
+ const profileCapturedScene = <
61
+ C extends UiContract,
62
+ S extends ReadonlyArray<unknown>,
63
+ Services,
64
+ P,
65
+ N extends string,
66
+ Identity,
67
+ >(
68
+ base: CapturedScene<C, S, Services, P, N, Identity>,
69
+ profiler: ProfilerHandle,
70
+ lifecycle: ReturnType<typeof makeLifecycle>,
71
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity> => ({
72
+ scene: withLifecycle(profiledScene(base, profiler), lifecycle),
73
+ profiler,
74
+ })
75
+
76
+ export function profileScene<
77
+ C extends UiContract,
78
+ S extends ReadonlyArray<unknown>,
79
+ Services,
80
+ P,
81
+ N extends string,
82
+ Identity,
83
+ >(
84
+ base: CapturedScene<C, S, Services, P, N, Identity>,
85
+ options?: ProfilerOptionsExternalApi,
86
+ ): CapturedProfiledScene<C, S, Services, P, N, Identity>
87
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
88
+ base: Scene<C, S>,
89
+ options?: ProfilerOptionsExternalApi,
90
+ ): ProfiledScene<C, S>
91
+ export function profileScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
15
92
  base: Scene<C, S>,
16
93
  options: ProfilerOptionsExternalApi = {},
17
- ): ProfiledScene<C, S> => {
94
+ ): ProfiledScene<C, S> {
18
95
  const profiler = makeProfiler({
19
96
  label: options.label ?? base.composition.manifest.title,
20
97
  ...(options.capacity !== undefined ? { capacity: options.capacity } : {}),
21
98
  })
22
- const lifecycle = Layer.scopedDiscard(
23
- Effect.acquireRelease(
24
- Effect.sync(() => registerProfiler({ label: profiler.label, handle: profiler })),
25
- (unregister) => Effect.sync(unregister),
26
- ),
99
+ const lifecycle = makeLifecycle(profiler)
100
+ return base.capture<ProfiledScene<C, S>>(
101
+ <Services, P, N extends string, Identity>(
102
+ exactScene: CapturedScene<C, S, Services, P, N, Identity>,
103
+ ): ProfiledScene<C, S> => profileCapturedScene(exactScene, profiler, lifecycle),
27
104
  )
28
- const wrapped = profiledScene(base, profiler)
29
- return {
30
- // Lifecycle on first provide layer so register/unregister track runtime build/dispose.
31
- scene: {
32
- ...wrapped,
33
- provide: wrapped.provide.map((layer, index) =>
34
- index === 0 ? Layer.merge(layer, lifecycle) : layer,
35
- ),
36
- },
37
- profiler,
38
- }
39
105
  }
@@ -114,19 +114,25 @@ class Bump extends Reducer.make('ProfilerBump', { states: [Count], events: [Bump
114
114
  const BumpLive = Reducer.live(Bump, (n) => n + 1)
115
115
 
116
116
  class RootUi extends ui('ProfilerRoot')<{ props: { count: number } }>() {}
117
+
117
118
  class Root extends Composition.make('ProfilerRoot', {
118
119
  title: 'ProfilerRoot',
119
120
  states: [States],
120
121
  events: [Bumped],
121
122
  ui: RootUi,
122
- }) {}
123
+ })<Root>() {}
123
124
  const RootLive = Composition.live(Root, function* () {
124
125
  const count = yield* StateGroup.select(States, 'profiler-count')
125
126
  return mount({ props: { count }, slots: {} })
126
127
  })
127
128
 
128
129
  const app = RootLive.pipe(
129
- Layer.provideMerge(provide(RootUi, Ui.make(RootUi, () => null))),
130
+ Layer.provideMerge(
131
+ provide(
132
+ RootUi,
133
+ Ui.make(RootUi, () => null),
134
+ ),
135
+ ),
130
136
  Layer.provideMerge(
131
137
  Layer.mergeAll(BumpLive).pipe(
132
138
  Layer.provideMerge(Layer.mergeAll(Engine, StateGroup.live(States, { 'profiler-count': 0 }))),
@@ -97,7 +97,9 @@ const DroppedNote = ({ count }: DroppedNoteProps): ReactNode => {
97
97
  if (count === 0) {
98
98
  return null
99
99
  }
100
- return <div style={{ color: palette.dim }}>({count} early records evicted from the ring buffer)</div>
100
+ return (
101
+ <div style={{ color: palette.dim }}>({count} early records evicted from the ring buffer)</div>
102
+ )
101
103
  }
102
104
 
103
105
  interface ProfilerBodyProps {
package/src/recorder.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Instrumentation, makeScheduler, type SpanEnd } from '@playfast/reform'
1
+ import { type Instrumentation, makeScheduler, type SpanEnd } from '@playfast/reform/internal'
2
2
 
3
3
  export type ProfileKind =
4
4
  | 'event'
@@ -93,8 +93,7 @@ export const makeProfiler = (options: ProfilerOptionsExternalApi = {}): Profiler
93
93
  readonly detail: string
94
94
  }
95
95
 
96
- const instant = (site: Site): void =>
97
- record({ ...site, start: performance.now(), dur: 0 })
96
+ const instant = (site: Site): void => record({ ...site, start: performance.now(), dur: 0 })
98
97
 
99
98
  const span = (site: Site): SpanEnd => {
100
99
  const start = performance.now()
package/src/registry.ts CHANGED
@@ -10,7 +10,10 @@ export interface ProfilerRegistration {
10
10
 
11
11
  interface RegistryState {
12
12
  // Refcount: StrictMode double-build shares one entry.
13
- readonly entries: Map<ProfilerHandle, { readonly registration: ProfilerRegistration; readonly count: number }>
13
+ readonly entries: Map<
14
+ ProfilerHandle,
15
+ { readonly registration: ProfilerRegistration; readonly count: number }
16
+ >
14
17
  readonly listeners: Set<() => void>
15
18
  readonly version: { current: number }
16
19
  readonly overlay: { owner: Option.Option<object> }
package/src/trace.ts CHANGED
@@ -54,7 +54,14 @@ const US_PER_MS = 1000
54
54
  export const toChromeTrace = (profiler: ProfilerHandle): ChromeTrace => {
55
55
  const { records } = profiler.snapshot()
56
56
  const meta: ReadonlyArray<ChromeTraceEvent> = [
57
- { ph: 'M', name: 'process_name', ts: 0, pid, tid: 0, args: { name: `reform: ${profiler.label}` } },
57
+ {
58
+ ph: 'M',
59
+ name: 'process_name',
60
+ ts: 0,
61
+ pid,
62
+ tid: 0,
63
+ args: { name: `reform: ${profiler.label}` },
64
+ },
58
65
  ...Object.values(lanes).map(
59
66
  (lane): ChromeTraceEvent => ({
60
67
  ph: 'M',
@@ -71,9 +78,19 @@ export const toChromeTrace = (profiler: ProfilerHandle): ChromeTrace => {
71
78
  const cat = record.kind
72
79
  const tid = lanes[record.kind].tid
73
80
  const args = record.detail === '' ? {} : { detail: record.detail }
81
+ const complete: ChromeTraceEvent = {
82
+ ph: 'X',
83
+ name: record.name,
84
+ cat,
85
+ ts: startUs,
86
+ dur: record.dur * US_PER_MS,
87
+ pid,
88
+ tid,
89
+ args,
90
+ }
74
91
  return instantKinds.has(record.kind)
75
92
  ? { ph: 'i', name: record.name, cat, ts: startUs, pid, tid, s: 't', args }
76
- : { ph: 'X', name: record.name, cat, ts: startUs, dur: record.dur * US_PER_MS, pid, tid, args }
93
+ : complete
77
94
  })
78
95
  return { traceEvents: [...meta, ...body], displayTimeUnit: 'ms' }
79
96
  }