@livestore/utils 0.4.0-dev.20 → 0.4.0-dev.21
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/dist/.tsbuildinfo.json +1 -1
- package/dist/NoopTracer.d.ts.map +1 -1
- package/dist/NoopTracer.js +4 -1
- package/dist/NoopTracer.js.map +1 -1
- package/dist/browser/Opfs/utils.d.ts +7 -4
- package/dist/browser/Opfs/utils.d.ts.map +1 -1
- package/dist/browser/Opfs/utils.js +30 -18
- package/dist/browser/Opfs/utils.js.map +1 -1
- package/dist/effect/Debug.d.ts +28 -30
- package/dist/effect/Debug.d.ts.map +1 -1
- package/dist/effect/Debug.js +280 -221
- package/dist/effect/Debug.js.map +1 -1
- package/dist/effect/Schema/debug-diff.test.js +1 -1
- package/dist/effect/Schema/debug-diff.test.js.map +1 -1
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +1 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/global.d.ts +3 -0
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js.map +1 -1
- package/dist/guards.d.ts +14 -0
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +14 -0
- package/dist/guards.js.map +1 -1
- package/dist/mod.d.ts +195 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +149 -4
- package/dist/mod.js.map +1 -1
- package/package.json +20 -20
- package/src/NoopTracer.ts +4 -1
- package/src/browser/Opfs/utils.ts +36 -20
- package/src/effect/Debug.ts +369 -292
- package/src/effect/Schema/debug-diff.test.ts +2 -2
- package/src/effect/mod.ts +3 -0
- package/src/global.ts +4 -0
- package/src/guards.ts +15 -0
- package/src/mod.ts +197 -5
package/src/effect/Debug.ts
CHANGED
|
@@ -1,375 +1,452 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
3
|
-
import { Duration, Schedule } from 'effect'
|
|
4
|
-
import * as Console from 'effect/Console'
|
|
1
|
+
import * as Cause from 'effect/Cause'
|
|
2
|
+
import * as Context from 'effect/Context'
|
|
5
3
|
import * as Effect from 'effect/Effect'
|
|
6
|
-
import
|
|
4
|
+
import type * as Exit from 'effect/Exit'
|
|
5
|
+
import type * as Fiber from 'effect/Fiber'
|
|
7
6
|
import * as Graph from 'effect/Graph'
|
|
8
|
-
import * as Layer from 'effect/Layer'
|
|
9
7
|
import * as Option from 'effect/Option'
|
|
8
|
+
import * as RuntimeFlags from 'effect/RuntimeFlags'
|
|
9
|
+
import * as Scope from 'effect/Scope'
|
|
10
|
+
import type * as Tracer from 'effect/Tracer'
|
|
11
|
+
|
|
12
|
+
interface SpanEvent {
|
|
13
|
+
readonly name: string
|
|
14
|
+
readonly startTime: bigint
|
|
15
|
+
readonly attributes?: Record<string, unknown>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface GraphNodeInfo {
|
|
19
|
+
readonly span: Tracer.AnySpan
|
|
20
|
+
readonly exitTag: 'Success' | 'Failure' | 'Interrupted' | undefined
|
|
21
|
+
readonly events: Array<SpanEvent>
|
|
22
|
+
}
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
readonly
|
|
24
|
+
export type MutableSpanGraph = Graph.MutableGraph<GraphNodeInfo, void>
|
|
25
|
+
export type MutableSpanGraphInfo = {
|
|
26
|
+
readonly graph: MutableSpanGraph
|
|
27
|
+
readonly nodeIdBySpanId: Map<string, number>
|
|
14
28
|
}
|
|
15
29
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const layerClientInMemoryGraph = Layer.effect(
|
|
26
|
-
C.Client,
|
|
27
|
-
Effect.gen(function* () {
|
|
28
|
-
const { mutableGraph, nodeIdBySpanId } = yield* DebugInfo
|
|
29
|
-
|
|
30
|
-
function ensureNode(traceId: string, spanId: string) {
|
|
31
|
-
const existingNodeId = nodeIdBySpanId.get(spanId)
|
|
32
|
-
if (existingNodeId !== undefined) return existingNodeId
|
|
33
|
-
const nodeId = Graph.addNode(mutableGraph, {
|
|
34
|
-
span: { _tag: 'ExternalSpan', spanId, traceId, sampled: false },
|
|
35
|
-
events: [],
|
|
36
|
-
})
|
|
37
|
-
nodeIdBySpanId.set(spanId, nodeId)
|
|
38
|
-
return nodeId
|
|
30
|
+
const graphByTraceId = new Map<string, MutableSpanGraphInfo>()
|
|
31
|
+
|
|
32
|
+
function ensureSpan(traceId: string, spanId: string): [MutableSpanGraph, number] {
|
|
33
|
+
let info = graphByTraceId.get(traceId)
|
|
34
|
+
if (info === undefined) {
|
|
35
|
+
info = {
|
|
36
|
+
graph: Graph.beginMutation(Graph.directed<GraphNodeInfo, void>()),
|
|
37
|
+
nodeIdBySpanId: new Map<string, number>(),
|
|
39
38
|
}
|
|
39
|
+
graphByTraceId.set(traceId, info)
|
|
40
|
+
}
|
|
41
|
+
let nodeId = info.nodeIdBySpanId.get(spanId)
|
|
42
|
+
if (nodeId === undefined) {
|
|
43
|
+
nodeId = Graph.addNode(info.graph, {
|
|
44
|
+
span: { _tag: 'ExternalSpan', spanId, traceId, sampled: false, context: Context.empty() },
|
|
45
|
+
events: [],
|
|
46
|
+
exitTag: undefined,
|
|
47
|
+
})
|
|
48
|
+
info.nodeIdBySpanId.set(spanId, nodeId)
|
|
49
|
+
}
|
|
50
|
+
return [info.graph, nodeId]
|
|
51
|
+
}
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
function sortSpan(
|
|
54
|
+
prev: Tracer.AnySpan,
|
|
55
|
+
next: Tracer.AnySpan,
|
|
56
|
+
): [info: Tracer.AnySpan, isUpgrade: boolean, timingUpdated: boolean] {
|
|
57
|
+
if (prev._tag === 'ExternalSpan' && next._tag === 'Span') return [next, true, true]
|
|
58
|
+
if (prev._tag === 'Span' && next._tag === 'Span' && next.status._tag === 'Ended') return [next, false, true]
|
|
59
|
+
return [prev, false, false]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addNode(span: Tracer.AnySpan) {
|
|
63
|
+
const [mutableGraph, nodeId] = ensureSpan(span.traceId, span.spanId)
|
|
64
|
+
Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
|
|
65
|
+
const [latestInfo, upgraded] = sortSpan(previousInfo.span, span)
|
|
66
|
+
if (upgraded && latestInfo._tag === 'Span' && Option.isSome(latestInfo.parent)) {
|
|
67
|
+
const parentNodeId = addNode(latestInfo.parent.value)
|
|
68
|
+
Graph.addEdge(mutableGraph, parentNodeId, nodeId, undefined)
|
|
46
69
|
}
|
|
70
|
+
return { ...previousInfo, span: latestInfo }
|
|
71
|
+
})
|
|
72
|
+
return nodeId
|
|
73
|
+
}
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
function addEvent(traceId: string, spanId: string, event: SpanEvent) {
|
|
76
|
+
const [mutableGraph, nodeId] = ensureSpan(traceId, spanId)
|
|
77
|
+
Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
|
|
78
|
+
return { ...previousInfo, events: [...previousInfo.events, event] }
|
|
79
|
+
})
|
|
80
|
+
return nodeId
|
|
81
|
+
}
|
|
82
|
+
function addNodeExit(traceId: string, spanId: string, exit: Exit.Exit<any, any>) {
|
|
83
|
+
const [mutableGraph, nodeId] = ensureSpan(traceId, spanId)
|
|
84
|
+
Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
|
|
85
|
+
const isInterruptedOnly = exit._tag === 'Failure' && Cause.isInterruptedOnly(exit.cause)
|
|
86
|
+
return {
|
|
87
|
+
...previousInfo,
|
|
88
|
+
exitTag: isInterruptedOnly ? ('Interrupted' as const) : exit._tag,
|
|
59
89
|
}
|
|
90
|
+
})
|
|
91
|
+
return nodeId
|
|
92
|
+
}
|
|
60
93
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
function createPropertyInterceptor<T extends object, K extends keyof T>(
|
|
95
|
+
obj: T,
|
|
96
|
+
property: K,
|
|
97
|
+
interceptor: (value: T[K]) => void,
|
|
98
|
+
): void {
|
|
99
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, property)
|
|
100
|
+
|
|
101
|
+
const previousSetter = descriptor?.set
|
|
102
|
+
|
|
103
|
+
let currentValue: T[K]
|
|
104
|
+
const previousGetter = descriptor?.get
|
|
105
|
+
|
|
106
|
+
if (!previousGetter) {
|
|
107
|
+
currentValue = obj[property]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Object.defineProperty(obj, property, {
|
|
111
|
+
get(): T[K] {
|
|
112
|
+
if (previousGetter) {
|
|
113
|
+
return previousGetter.call(obj)
|
|
114
|
+
}
|
|
115
|
+
return currentValue
|
|
116
|
+
},
|
|
117
|
+
set(value: T[K]) {
|
|
118
|
+
if (previousSetter) {
|
|
119
|
+
previousSetter.call(obj, value)
|
|
120
|
+
} else {
|
|
121
|
+
currentValue = value
|
|
122
|
+
}
|
|
123
|
+
interceptor(value)
|
|
124
|
+
},
|
|
125
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
126
|
+
configurable: descriptor?.configurable ?? true,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type EffectDevtoolsHookEvent =
|
|
131
|
+
| {
|
|
132
|
+
_tag: 'FiberAllocated'
|
|
133
|
+
fiber: Fiber.RuntimeFiber<any, any>
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
_tag: 'ScopeAllocated'
|
|
137
|
+
scope: Scope.Scope
|
|
67
138
|
}
|
|
68
139
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
140
|
+
type GlobalWithFiberCurrent = {
|
|
141
|
+
'effect/FiberCurrent': Fiber.RuntimeFiber<any, any> | undefined
|
|
142
|
+
'effect/DevtoolsHook'?: {
|
|
143
|
+
onEvent: (event: EffectDevtoolsHookEvent) => void
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const patchedTracer = new WeakSet<Tracer.Tracer>()
|
|
148
|
+
function ensureTracerPatched(currentTracer: Tracer.Tracer) {
|
|
149
|
+
if (patchedTracer.has(currentTracer)) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
patchedTracer.add(currentTracer)
|
|
81
153
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
154
|
+
const oldSpanConstructor = currentTracer.span
|
|
155
|
+
currentTracer.span = function (...args) {
|
|
156
|
+
const span = oldSpanConstructor.apply(this, args)
|
|
157
|
+
addNode(span)
|
|
158
|
+
|
|
159
|
+
const oldSpanEnd = span.end
|
|
160
|
+
span.end = function (endTime, exit, ...args) {
|
|
161
|
+
oldSpanEnd.apply(this, [endTime, exit, ...args])
|
|
162
|
+
addNodeExit(this.traceId, this.spanId, exit)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const oldSpanEvent = span.event
|
|
166
|
+
span.event = function (name, startTime, attributes, ...args) {
|
|
167
|
+
oldSpanEvent.apply(this, [name, startTime, attributes, ...args])
|
|
168
|
+
addEvent(this.traceId, this.spanId, { name, startTime, attributes: attributes ?? {} })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return span
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const oldContext = currentTracer.context
|
|
175
|
+
currentTracer.context = function (f, fiber, ...args) {
|
|
176
|
+
const context = oldContext.apply(this, [f, fiber, ...args])
|
|
177
|
+
ensureFiberPatched(fiber)
|
|
178
|
+
return context as any
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface ScopeImpl extends Scope.Scope {
|
|
183
|
+
readonly state:
|
|
184
|
+
| {
|
|
185
|
+
readonly _tag: 'Open'
|
|
186
|
+
readonly finalizers: Map<{}, Scope.Scope.Finalizer>
|
|
187
|
+
}
|
|
188
|
+
| {
|
|
189
|
+
readonly _tag: 'Closed'
|
|
190
|
+
readonly exit: Exit.Exit<unknown, unknown>
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const knownScopes = new Map<
|
|
195
|
+
ScopeImpl,
|
|
196
|
+
{ id: number; allocationFiber: Fiber.RuntimeFiber<any, any> | undefined; allocationSpan: Tracer.AnySpan | undefined }
|
|
197
|
+
>()
|
|
198
|
+
let lastScopeId = 0
|
|
199
|
+
function ensureScopePatched(scope: ScopeImpl, allocationFiber: Fiber.RuntimeFiber<any, any> | undefined) {
|
|
200
|
+
if (scope.state._tag === 'Closed') return
|
|
201
|
+
if (knownScopes.has(scope)) return
|
|
202
|
+
const id = lastScopeId++
|
|
203
|
+
if (patchScopeClose) {
|
|
204
|
+
const oldClose = (scope as any).close
|
|
205
|
+
;(scope as any).close = function (...args: any[]) {
|
|
206
|
+
return oldClose.apply(this as any, args).pipe(
|
|
207
|
+
Effect.withSpan(`scope.${id}.closeRunFinalizers`),
|
|
208
|
+
Effect.ensuring(
|
|
209
|
+
Effect.sync(() => {
|
|
210
|
+
knownScopes.delete(scope)
|
|
211
|
+
}),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
cleanupScopes()
|
|
217
|
+
}
|
|
218
|
+
const allocationSpan = allocationFiber?.currentSpan
|
|
219
|
+
knownScopes.set(scope, { id, allocationFiber, allocationSpan })
|
|
220
|
+
}
|
|
221
|
+
const cleanupScopes = () => {
|
|
222
|
+
for (const [scope] of knownScopes) {
|
|
223
|
+
if (scope.state._tag === 'Closed') knownScopes.delete(scope)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const knownFibers = new Set<Fiber.RuntimeFiber<any, any>>()
|
|
228
|
+
function ensureFiberPatched(fiber: Fiber.RuntimeFiber<any, any>) {
|
|
229
|
+
// patch tracer
|
|
230
|
+
ensureTracerPatched(fiber.currentTracer)
|
|
231
|
+
// patch scope
|
|
232
|
+
const currentScope = Context.getOrElse(fiber.currentContext, Scope.Scope, () => undefined)
|
|
233
|
+
if (currentScope) ensureScopePatched(currentScope as any as ScopeImpl, undefined)
|
|
234
|
+
// patch fiber
|
|
235
|
+
if (knownFibers.has(fiber)) return
|
|
236
|
+
knownFibers.add(fiber)
|
|
237
|
+
fiber.addObserver((exit) => {
|
|
238
|
+
knownFibers.delete(fiber)
|
|
239
|
+
onFiberCompleted?.(fiber, exit)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let patchScopeClose = false
|
|
244
|
+
let onFiberResumed: undefined | ((fiber: Fiber.RuntimeFiber<any, any>) => void)
|
|
245
|
+
let onFiberSuspended: undefined | ((fiber: Fiber.RuntimeFiber<any, any>) => void)
|
|
246
|
+
let onFiberCompleted: undefined | ((fiber: Fiber.RuntimeFiber<any, any>, exit: Exit.Exit<any, any>) => void)
|
|
247
|
+
export function attachSlowDebugInstrumentation(options: {
|
|
248
|
+
/** If set to true, the scope prototype will be patched to attach a span to visualize pending scope closing */
|
|
249
|
+
readonly patchScopeClose?: boolean
|
|
250
|
+
/** An optional callback that will be called when any fiber resumes performing a run loop */
|
|
251
|
+
readonly onFiberResumed?: (fiber: Fiber.RuntimeFiber<any, any>) => void
|
|
252
|
+
/** An optional callback that will be called when any fiber stops performing a run loop */
|
|
253
|
+
readonly onFiberSuspended?: (fiber: Fiber.RuntimeFiber<any, any>) => void
|
|
254
|
+
/** An optional callback that will be called when any fiber completes with a exit */
|
|
255
|
+
readonly onFiberCompleted?: (fiber: Fiber.RuntimeFiber<any, any>, exit: Exit.Exit<any, any>) => void
|
|
256
|
+
}) {
|
|
257
|
+
const _globalThis = globalThis as any as GlobalWithFiberCurrent
|
|
258
|
+
if (_globalThis['effect/DevtoolsHook']) {
|
|
259
|
+
return console.error(
|
|
260
|
+
'attachDebugInstrumentation has already been called! To show the tree, call attachDebugInstrumentation() in the root/main file of your program to ensure it is loaded as soon as possible.',
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
patchScopeClose = options.patchScopeClose ?? false
|
|
264
|
+
onFiberResumed = options.onFiberResumed
|
|
265
|
+
onFiberSuspended = options.onFiberSuspended
|
|
266
|
+
onFiberCompleted = options.onFiberCompleted
|
|
267
|
+
let lastFiber: undefined | Fiber.RuntimeFiber<any, any>
|
|
268
|
+
createPropertyInterceptor(globalThis as any as GlobalWithFiberCurrent, 'effect/FiberCurrent', (value) => {
|
|
269
|
+
if (value && knownFibers.has(value)) onFiberResumed?.(value)
|
|
270
|
+
if (value) ensureFiberPatched(value)
|
|
271
|
+
if (!value && lastFiber && knownFibers.has(lastFiber)) onFiberSuspended?.(lastFiber)
|
|
272
|
+
lastFiber = value
|
|
273
|
+
})
|
|
274
|
+
_globalThis['effect/DevtoolsHook'] = {
|
|
275
|
+
onEvent: (event) => {
|
|
276
|
+
console.log('onEvent', event)
|
|
277
|
+
switch (event._tag) {
|
|
278
|
+
case 'ScopeAllocated':
|
|
279
|
+
ensureScopePatched(event.scope as any as ScopeImpl, _globalThis['effect/FiberCurrent'])
|
|
280
|
+
break
|
|
281
|
+
case 'FiberAllocated':
|
|
282
|
+
ensureFiberPatched(event.fiber)
|
|
283
|
+
break
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
89
288
|
|
|
90
289
|
function formatDuration(startTime: bigint, endTime: bigint | undefined): string {
|
|
91
|
-
if (endTime === undefined) return 'running'
|
|
290
|
+
if (endTime === undefined) return '[running]'
|
|
92
291
|
const durationMs = Number(endTime - startTime) / 1000000 // Convert nanoseconds to milliseconds
|
|
93
292
|
if (durationMs < 1000) return `${durationMs.toFixed(0)}ms`
|
|
94
293
|
if (durationMs < 60000) return `${(durationMs / 1000).toFixed(2)}s`
|
|
95
294
|
return `${(durationMs / 60000).toFixed(2)}m`
|
|
96
295
|
}
|
|
97
296
|
|
|
98
|
-
function getSpanName(span:
|
|
297
|
+
function getSpanName(span: Tracer.AnySpan): string {
|
|
99
298
|
if (span._tag === 'ExternalSpan') return `[external] ${span.spanId}`
|
|
100
299
|
return span.name
|
|
101
300
|
}
|
|
102
301
|
|
|
103
|
-
function getSpanStatus(
|
|
104
|
-
if (span._tag === 'ExternalSpan') return '?'
|
|
105
|
-
if (
|
|
106
|
-
return '
|
|
302
|
+
function getSpanStatus(info: GraphNodeInfo): string {
|
|
303
|
+
if (info.span._tag === 'ExternalSpan') return '?'
|
|
304
|
+
if (info.exitTag === 'Success') return '✓'
|
|
305
|
+
if (info.exitTag === 'Failure') return '✗'
|
|
306
|
+
if (info.exitTag === 'Interrupted') return '!'
|
|
307
|
+
return '⋮'
|
|
107
308
|
}
|
|
108
309
|
|
|
109
|
-
function getSpanDuration(span:
|
|
310
|
+
function getSpanDuration(span: Tracer.AnySpan): string {
|
|
110
311
|
if (span._tag === 'ExternalSpan') return ''
|
|
111
312
|
const endTime = span.status._tag === 'Ended' ? span.status.endTime : undefined
|
|
112
313
|
return formatDuration(span.status.startTime, endTime)
|
|
113
314
|
}
|
|
114
315
|
|
|
115
|
-
|
|
116
|
-
* Filters a graph by keeping only nodes that match the predicate or have descendants that match.
|
|
117
|
-
* This ensures parent nodes are included even if they don't match, as long as they have matching descendants.
|
|
118
|
-
*
|
|
119
|
-
* @param graph - The graph to filter
|
|
120
|
-
* @param predicate - A function that tests whether a node should be included
|
|
121
|
-
* @returns A new filtered graph containing only matching nodes and their ancestors
|
|
122
|
-
*/
|
|
123
|
-
function filterGraphWithAncestors<N, E>(
|
|
316
|
+
function filterGraphKeepAncestors<N, E>(
|
|
124
317
|
graph: Graph.Graph<N, E>,
|
|
125
318
|
predicate: (nodeData: N, nodeId: number) => boolean,
|
|
126
319
|
): Graph.Graph<N, E> {
|
|
127
320
|
// Find all root nodes (nodes with no incoming edges)
|
|
128
321
|
const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
|
|
129
|
-
|
|
130
|
-
const shouldInclude = new Map<number, boolean>()
|
|
322
|
+
const shouldInclude = new Set<number>()
|
|
131
323
|
|
|
132
324
|
// Use postorder DFS to evaluate children before parents
|
|
133
|
-
for (const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
325
|
+
for (const nodeId of Graph.indices(Graph.dfsPostOrder(graph, { start: rootNodes, direction: 'outgoing' }))) {
|
|
326
|
+
const node = Graph.getNode(graph, nodeId)
|
|
327
|
+
if (Option.isNone(node)) continue
|
|
328
|
+
|
|
329
|
+
const matchesPredicate = predicate(node.value, nodeId)
|
|
330
|
+
if (matchesPredicate) {
|
|
331
|
+
shouldInclude.add(nodeId)
|
|
332
|
+
} else {
|
|
141
333
|
const children = Graph.neighborsDirected(graph, nodeId, 'outgoing')
|
|
142
|
-
const hasMatchingChildren = children.some((childId) => shouldInclude.
|
|
143
|
-
|
|
144
|
-
const include = matchesPredicate || hasMatchingChildren
|
|
145
|
-
shouldInclude.set(nodeId, include)
|
|
334
|
+
const hasMatchingChildren = children.some((childId) => shouldInclude.has(childId))
|
|
335
|
+
if (hasMatchingChildren) shouldInclude.add(nodeId)
|
|
146
336
|
}
|
|
147
337
|
}
|
|
148
338
|
|
|
149
339
|
// Create a filtered copy of the graph
|
|
150
340
|
return Graph.mutate(graph, (mutable) => {
|
|
151
341
|
for (const [nodeId] of mutable.nodes) {
|
|
152
|
-
if (shouldInclude.
|
|
342
|
+
if (shouldInclude.has(nodeId)) continue
|
|
153
343
|
Graph.removeNode(mutable, nodeId)
|
|
154
344
|
}
|
|
155
345
|
})
|
|
156
346
|
}
|
|
157
347
|
|
|
158
|
-
function
|
|
348
|
+
function renderSpanNode(graph: Graph.Graph<GraphNodeInfo, void, 'directed'>, nodeId: number): string[] {
|
|
159
349
|
const node = Graph.getNode(graph, nodeId)
|
|
160
350
|
if (Option.isNone(node)) return []
|
|
161
|
-
|
|
162
351
|
const info = node.value
|
|
163
|
-
const status = getSpanStatus(info
|
|
352
|
+
const status = getSpanStatus(info)
|
|
164
353
|
const name = getSpanName(info.span)
|
|
165
354
|
const duration = getSpanDuration(info.span)
|
|
166
355
|
const durationStr = duration ? ` ${duration}` : ''
|
|
167
356
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
children.forEach((childId: number, index: number) => {
|
|
176
|
-
const isLastChild = index === childCount - 1
|
|
177
|
-
const childPrefix = prefix + (isLast ? ' ' : '│ ')
|
|
178
|
-
const childLines = renderNode(graph, childId, childPrefix, isLastChild)
|
|
179
|
-
lines.push(...childLines)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
return lines
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
interface ChromeTraceEvent {
|
|
186
|
-
name: string
|
|
187
|
-
cat: string
|
|
188
|
-
ph: string // Phase: 'B' for begin, 'E' for end
|
|
189
|
-
ts: number // Timestamp in microseconds
|
|
190
|
-
pid: number // Process ID
|
|
191
|
-
tid: number // Thread ID
|
|
192
|
-
args?: Record<string, unknown>
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Converts a span node to Chrome Trace Events (Begin and End)
|
|
197
|
-
*/
|
|
198
|
-
function spanToTraceEvents(span: Domain.ParentSpan, _nodeId: number, processId = 1): ChromeTraceEvent[] {
|
|
199
|
-
const events: ChromeTraceEvent[] = []
|
|
200
|
-
|
|
201
|
-
if (span._tag === 'ExternalSpan') {
|
|
202
|
-
// External spans don't have timing info, skip them
|
|
203
|
-
return events
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const name = span.name
|
|
207
|
-
const threadId = 1 // Could use spanId hash or other identifier
|
|
208
|
-
|
|
209
|
-
// Begin event
|
|
210
|
-
const startTimeUs = Number(span.status.startTime) / 1000 // Convert nanoseconds to microseconds
|
|
211
|
-
events.push({
|
|
212
|
-
name,
|
|
213
|
-
cat: 'span',
|
|
214
|
-
ph: 'B',
|
|
215
|
-
ts: startTimeUs,
|
|
216
|
-
pid: processId,
|
|
217
|
-
tid: threadId,
|
|
218
|
-
args: {
|
|
219
|
-
spanId: span.spanId,
|
|
220
|
-
traceId: span.traceId,
|
|
221
|
-
attributes: Object.fromEntries(span.attributes),
|
|
222
|
-
},
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// End event (only if span has ended)
|
|
226
|
-
if (span.status._tag === 'Ended') {
|
|
227
|
-
const endTimeUs = Number(span.status.endTime) / 1000 // Convert nanoseconds to microseconds
|
|
228
|
-
events.push({
|
|
229
|
-
name,
|
|
230
|
-
cat: 'span',
|
|
231
|
-
ph: 'E',
|
|
232
|
-
ts: endTimeUs,
|
|
233
|
-
pid: processId,
|
|
234
|
-
tid: threadId,
|
|
235
|
-
})
|
|
236
|
-
}
|
|
357
|
+
const fiberIds = Array.from(knownFibers)
|
|
358
|
+
.filter(
|
|
359
|
+
(fiber) => fiber.currentSpan?.spanId === info.span.spanId && fiber.currentSpan?.traceId === info.span.traceId,
|
|
360
|
+
)
|
|
361
|
+
.map((fiber) => `#${fiber.id().id}`)
|
|
362
|
+
.join(', ')
|
|
363
|
+
const runningOnFibers = fiberIds.length > 0 ? ` [fibers ${fiberIds}]` : ''
|
|
237
364
|
|
|
238
|
-
return
|
|
365
|
+
return [` ${status} ${name}${durationStr}${runningOnFibers}`]
|
|
239
366
|
}
|
|
240
367
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
368
|
+
function renderTree<N, E, T extends Graph.Kind>(
|
|
369
|
+
graph: Graph.Graph<N, E, T>,
|
|
370
|
+
nodeIds: Array<number>,
|
|
371
|
+
renderNode: (graph: Graph.Graph<N, E, T>, nodeId: number) => string[],
|
|
372
|
+
): string[] {
|
|
373
|
+
let lines: string[] = []
|
|
374
|
+
for (let childIndex = 0; childIndex < nodeIds.length; childIndex++) {
|
|
375
|
+
const isLastChild = childIndex === nodeIds.length - 1
|
|
376
|
+
const childLines = renderNode(graph, nodeIds[childIndex]!).concat(
|
|
377
|
+
renderTree(graph, Graph.neighborsDirected(graph, nodeIds[childIndex]!, 'outgoing'), renderNode),
|
|
378
|
+
)
|
|
379
|
+
lines = [
|
|
380
|
+
...lines,
|
|
381
|
+
...childLines.map((l, lineIndex) => {
|
|
382
|
+
if (lineIndex === 0) {
|
|
383
|
+
return (isLastChild ? ' └─' : ' ├─') + l
|
|
384
|
+
}
|
|
385
|
+
return (isLastChild ? ' ' : ' │') + l
|
|
386
|
+
}),
|
|
387
|
+
]
|
|
256
388
|
}
|
|
257
389
|
|
|
258
|
-
|
|
259
|
-
events.sort((a, b) => a.ts - b.ts)
|
|
260
|
-
|
|
261
|
-
return events
|
|
390
|
+
return lines
|
|
262
391
|
}
|
|
263
392
|
|
|
264
|
-
export interface
|
|
393
|
+
export interface LogDebugOptions {
|
|
265
394
|
readonly regex?: RegExp
|
|
266
395
|
readonly title?: string
|
|
267
396
|
}
|
|
268
397
|
|
|
269
|
-
export const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const { mutableGraph } = maybeInfo.value
|
|
278
|
-
const graph = Graph.endMutation(mutableGraph)
|
|
398
|
+
export const logDebug = (options: LogDebugOptions = {}) => {
|
|
399
|
+
const _globalThis = globalThis as any as GlobalWithFiberCurrent
|
|
400
|
+
if (!_globalThis['effect/DevtoolsHook']) {
|
|
401
|
+
return console.error(
|
|
402
|
+
'attachDebugInstrumentation has not been called! To show the tree, call attachDebugInstrumentation() in the root/main file of your program to ensure it is loaded as soon as possible.',
|
|
403
|
+
)
|
|
404
|
+
}
|
|
279
405
|
|
|
280
|
-
|
|
281
|
-
const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
|
|
406
|
+
let lines: Array<string> = [`----------------${options.title ?? ''}----------------`]
|
|
282
407
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
408
|
+
// fibers
|
|
409
|
+
lines = [...lines, 'Active Fibers:']
|
|
410
|
+
for (const fiber of knownFibers) {
|
|
411
|
+
const interruptible = RuntimeFlags.interruptible((fiber as any).currentRuntimeFlags)
|
|
412
|
+
lines = [...lines, `- #${fiber.id().id}${!interruptible ? ' [uninterruptible]' : ''}`]
|
|
413
|
+
}
|
|
414
|
+
if (knownFibers.size === 0) {
|
|
415
|
+
lines = [...lines, '- No active effect fibers']
|
|
416
|
+
}
|
|
417
|
+
lines = [...lines, '']
|
|
286
418
|
|
|
287
|
-
|
|
419
|
+
// spans
|
|
420
|
+
for (const [traceId, info] of graphByTraceId) {
|
|
421
|
+
const graph = Graph.endMutation(info.graph)
|
|
288
422
|
const filteredGraph = options.regex
|
|
289
|
-
?
|
|
423
|
+
? filterGraphKeepAncestors(graph, (nodeData, _nodeId) => {
|
|
290
424
|
const name = getSpanName(nodeData.span)
|
|
291
425
|
return options.regex!.test(name)
|
|
292
426
|
})
|
|
293
427
|
: graph
|
|
294
|
-
|
|
295
|
-
// Find root nodes in the filtered graph
|
|
296
428
|
const filteredRootNodes = Array.from(Graph.indices(Graph.externals(filteredGraph, { direction: 'incoming' })))
|
|
297
429
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
})
|
|
430
|
+
lines = [...lines, `Spans Trace ${traceId}:`, ...renderTree(filteredGraph, filteredRootNodes, renderSpanNode)]
|
|
431
|
+
}
|
|
432
|
+
lines = [...lines, '? external span - ✓ success - ✗ failure - ! interrupted', '']
|
|
433
|
+
|
|
434
|
+
// scopes
|
|
435
|
+
lines = [...lines, 'Open Scopes:']
|
|
436
|
+
for (const [scope, info] of knownScopes) {
|
|
437
|
+
const fiberIds = Array.from(knownFibers)
|
|
438
|
+
.filter((fiber) => Context.getOrElse(fiber.currentContext, Scope.Scope, () => undefined) === scope)
|
|
439
|
+
.map((fiber) => `#${fiber.id().id}`)
|
|
440
|
+
.join(', ')
|
|
441
|
+
const usedByFibers = fiberIds.length > 0 ? ` [used by: ${fiberIds}]` : ''
|
|
442
|
+
const allocationFiber = info.allocationFiber ? ` [allocated in fiber #${info.allocationFiber.id().id}]` : ''
|
|
443
|
+
const allocationSpan = info.allocationSpan ? ` [allocated in span: ${getSpanName(info.allocationSpan)}]` : ''
|
|
444
|
+
lines = [...lines, `- #${info.id}${usedByFibers}${allocationFiber}${allocationSpan}`]
|
|
445
|
+
}
|
|
446
|
+
if (knownScopes.size === 0) {
|
|
447
|
+
lines = [...lines, '- No active scopes']
|
|
448
|
+
}
|
|
449
|
+
lines = [...lines, '']
|
|
319
450
|
|
|
320
|
-
|
|
321
|
-
readonly regex?: RegExp
|
|
451
|
+
console.log(lines.join('\n'))
|
|
322
452
|
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Logs the span tree in Chrome Performance Trace Event Format.
|
|
326
|
-
* The output can be loaded in Chrome DevTools Performance tab or chrome://tracing
|
|
327
|
-
*
|
|
328
|
-
* @see https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
|
|
329
|
-
*/
|
|
330
|
-
export const logPerformanceTrace = (options: LogPerformanceTraceOptions = {}) =>
|
|
331
|
-
Effect.gen(function* () {
|
|
332
|
-
const maybeInfo = yield* Effect.serviceOption(DebugInfo)
|
|
333
|
-
if (Option.isNone(maybeInfo)) {
|
|
334
|
-
return yield* Console.log(
|
|
335
|
-
'(no debug info provided! To show the trace, provide the layerDebug layer in the root of your program)',
|
|
336
|
-
)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const { mutableGraph } = maybeInfo.value
|
|
340
|
-
const graph = Graph.endMutation(mutableGraph)
|
|
341
|
-
|
|
342
|
-
// Find root nodes (nodes with no incoming edges) using externals
|
|
343
|
-
const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
|
|
344
|
-
|
|
345
|
-
if (rootNodes.length === 0) {
|
|
346
|
-
return yield* Console.log('[]')
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Apply filter if provided
|
|
350
|
-
const filteredGraph = options.regex
|
|
351
|
-
? filterGraphWithAncestors(graph, (nodeData, _nodeId) => {
|
|
352
|
-
const name = getSpanName(nodeData.span)
|
|
353
|
-
return options.regex!.test(name)
|
|
354
|
-
})
|
|
355
|
-
: graph
|
|
356
|
-
|
|
357
|
-
// Find root nodes in the filtered graph
|
|
358
|
-
const filteredRootNodes = Array.from(Graph.indices(Graph.externals(filteredGraph, { direction: 'incoming' })))
|
|
359
|
-
|
|
360
|
-
// Collect all trace events
|
|
361
|
-
const events = collectTraceEvents(filteredGraph, filteredRootNodes)
|
|
362
|
-
|
|
363
|
-
// Output as compact JSON array
|
|
364
|
-
const json = JSON.stringify(events)
|
|
365
|
-
return yield* Console.log(json)
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
export const logScopeState = ({ label }: { label: string }) =>
|
|
369
|
-
Effect.gen(function* () {
|
|
370
|
-
const scope = yield* Effect.scope
|
|
371
|
-
yield* Effect.log(`scope.state[${label}]`, (scope as any).state._tag).pipe(
|
|
372
|
-
Effect.repeat({ schedule: Schedule.fixed(Duration.millis(100)) }),
|
|
373
|
-
Effect.forkScoped,
|
|
374
|
-
)
|
|
375
|
-
})
|