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