@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 +17 -17
- package/src/index.ts +1 -1
- package/src/noop.ts +48 -12
- package/src/profileScene.ts +85 -19
- package/src/profiler.test.ts +8 -2
- package/src/react/overlay.tsx +3 -1
- package/src/recorder.ts +2 -3
- package/src/registry.ts +4 -1
- package/src/trace.ts +19 -2
package/package.json
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-profiler",
|
|
3
|
-
"
|
|
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
|
-
"
|
|
6
|
+
"devtools",
|
|
9
7
|
"effect",
|
|
10
|
-
"profiler",
|
|
11
8
|
"performance",
|
|
12
|
-
"
|
|
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
|
-
"
|
|
21
|
-
"
|
|
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
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
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
|
-
"
|
|
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 {
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
|
|
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
|
-
|
|
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>
|
|
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 =
|
|
96
|
+
export const registerProfiler =
|
|
97
|
+
(_registration: ProfilerRegistration): (() => void) =>
|
|
98
|
+
() => {}
|
|
65
99
|
|
|
66
100
|
export const listProfilers = (): ReadonlyArray<ProfilerRegistration> => []
|
|
67
101
|
|
|
68
|
-
export const subscribeProfilers =
|
|
102
|
+
export const subscribeProfilers =
|
|
103
|
+
(_listener: () => void): (() => void) =>
|
|
104
|
+
() => {}
|
|
69
105
|
|
|
70
106
|
export const profilersVersion = (): number => 0
|
|
71
107
|
|
package/src/profileScene.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
}
|
package/src/profiler.test.ts
CHANGED
|
@@ -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(
|
|
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 }))),
|
package/src/react/overlay.tsx
CHANGED
|
@@ -97,7 +97,9 @@ const DroppedNote = ({ count }: DroppedNoteProps): ReactNode => {
|
|
|
97
97
|
if (count === 0) {
|
|
98
98
|
return null
|
|
99
99
|
}
|
|
100
|
-
return
|
|
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<
|
|
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
|
-
{
|
|
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
|
-
:
|
|
93
|
+
: complete
|
|
77
94
|
})
|
|
78
95
|
return { traceEvents: [...meta, ...body], displayTimeUnit: 'ms' }
|
|
79
96
|
}
|