@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.
@@ -1,375 +1,452 @@
1
- import * as C from '@effect/experimental/DevTools/Client'
2
- import type * as Domain from '@effect/experimental/DevTools/Domain'
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 { pipe } from 'effect/Function'
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
- interface SpanNodeInfo {
12
- readonly span: Domain.ParentSpan
13
- readonly events: readonly Domain.SpanEvent[]
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
- export type SpanNodeGraph = Graph.Graph<SpanNodeInfo, any>
17
-
18
- class DebugInfo extends Effect.Service<DebugInfo>()('@mattiamanzati/debug/DebugInfo', {
19
- effect: Effect.sync(() => ({
20
- mutableGraph: Graph.beginMutation(Graph.directed<SpanNodeInfo, any>()),
21
- nodeIdBySpanId: new Map<string, number>(),
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
- function upgradeInfo(prev: Domain.ParentSpan, next: Domain.ParentSpan): [Domain.ParentSpan, boolean] {
42
- if (prev._tag === 'ExternalSpan' && next._tag === 'Span') return [next, true]
43
- if (prev._tag === 'Span' && next._tag === 'ExternalSpan') return [prev, false]
44
- if (prev._tag === 'Span' && prev.status._tag === 'Ended') return [prev, false]
45
- return [next, false]
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
- function addNode(span: Domain.ParentSpan) {
49
- const nodeId = ensureNode(span.traceId, span.spanId)
50
- Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
51
- const [latestInfo, upgraded] = upgradeInfo(previousInfo.span, span)
52
- if (upgraded && latestInfo._tag === 'Span' && Option.isSome(latestInfo.parent)) {
53
- const parentNodeId = addNode(latestInfo.parent.value)
54
- Graph.addEdge(mutableGraph, parentNodeId, nodeId, undefined)
55
- }
56
- return { ...previousInfo, span: latestInfo }
57
- })
58
- return nodeId
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
- function addEvent(event: Domain.SpanEvent) {
62
- const nodeId = ensureNode(event.traceId, event.spanId)
63
- Graph.updateNode(mutableGraph, nodeId, (previousInfo) => ({
64
- ...previousInfo,
65
- events: [...previousInfo.events, event],
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
- return C.Client.of({
70
- unsafeAddSpan: (span) => {
71
- switch (span._tag) {
72
- case 'SpanEvent':
73
- return addEvent(span)
74
- case 'Span':
75
- return addNode(span)
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
- export const layerDebug = pipe(
83
- C.makeTracer,
84
- Effect.map(Layer.setTracer),
85
- Layer.unwrapEffect,
86
- Layer.provide(layerClientInMemoryGraph),
87
- Layer.provideMerge(DebugInfo.Default),
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: Domain.ParentSpan): string {
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(span: Domain.ParentSpan): string {
104
- if (span._tag === 'ExternalSpan') return '?'
105
- if (span.status._tag === 'Ended') return '✓'
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: Domain.ParentSpan): string {
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 rootId of rootNodes) {
134
- for (const nodeId of Graph.indices(Graph.dfsPostOrder(graph, { startNodes: [rootId], direction: 'outgoing' }))) {
135
- const node = Graph.getNode(graph, nodeId)
136
- if (Option.isNone(node)) continue
137
-
138
- const matchesPredicate = predicate(node.value, nodeId)
139
-
140
- // Check if any children should be included
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.get(childId) === true)
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.get(nodeId) === true) continue
342
+ if (shouldInclude.has(nodeId)) continue
153
343
  Graph.removeNode(mutable, nodeId)
154
344
  }
155
345
  })
156
346
  }
157
347
 
158
- function renderNode(graph: SpanNodeGraph, nodeId: number, prefix = '', isLast = true): string[] {
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.span)
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 connector = isLast ? '└─ ' : '├─ '
169
- const lines: string[] = [`${prefix}${connector}${status} ${name}${durationStr}`]
170
-
171
- // Get children
172
- const children = Graph.neighborsDirected(graph, nodeId, 'outgoing')
173
- const childCount = children.length
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 events
365
+ return [` ${status} ${name}${durationStr}${runningOnFibers}`]
239
366
  }
240
367
 
241
- /**
242
- * Traverses the graph and collects all trace events
243
- */
244
- function collectTraceEvents(graph: SpanNodeGraph, rootNodes: number[]): ChromeTraceEvent[] {
245
- const events: ChromeTraceEvent[] = []
246
-
247
- // Use DFS to traverse all nodes
248
- for (const rootId of rootNodes) {
249
- for (const nodeId of Graph.indices(Graph.dfs(graph, { startNodes: [rootId], direction: 'outgoing' }))) {
250
- const node = Graph.getNode(graph, nodeId)
251
- if (Option.isNone(node)) continue
252
-
253
- const nodeEvents = spanToTraceEvents(node.value.span, nodeId)
254
- events.push(...nodeEvents)
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
- // Sort events by timestamp
259
- events.sort((a, b) => a.ts - b.ts)
260
-
261
- return events
390
+ return lines
262
391
  }
263
392
 
264
- export interface LogTreeOptions {
393
+ export interface LogDebugOptions {
265
394
  readonly regex?: RegExp
266
395
  readonly title?: string
267
396
  }
268
397
 
269
- export const logTree = (options: LogTreeOptions = {}) =>
270
- Effect.gen(function* () {
271
- const maybeInfo = yield* Effect.serviceOption(DebugInfo)
272
- if (Option.isNone(maybeInfo))
273
- return yield* Console.log(
274
- '(no debug info provided! To show the tree, provide the layerDebug layer in the root of your program)',
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
- // Find root nodes (nodes with no incoming edges) using externals
281
- const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
406
+ let lines: Array<string> = [`----------------${options.title ?? ''}----------------`]
282
407
 
283
- if (rootNodes.length === 0) {
284
- return yield* Console.log('(empty trace)')
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
- // Apply filter to create a filtered copy of the graph
419
+ // spans
420
+ for (const [traceId, info] of graphByTraceId) {
421
+ const graph = Graph.endMutation(info.graph)
288
422
  const filteredGraph = options.regex
289
- ? filterGraphWithAncestors(graph, (nodeData, _nodeId) => {
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
- if (filteredRootNodes.length === 0) {
299
- return yield* Console.log(options.title ? `${options.title}\n(no matches)` : '(no matches)')
300
- }
301
-
302
- const lines: string[] = []
303
-
304
- // Add title if provided
305
- if (options.title) {
306
- lines.push(options.title)
307
- }
308
-
309
- // Render root nodes using the same logic as regular children
310
- const rootCount = filteredRootNodes.length
311
- filteredRootNodes.forEach((rootId: number, index: number) => {
312
- const isLastRoot = index === rootCount - 1
313
- const rootLines = renderNode(filteredGraph, rootId, '', isLastRoot)
314
- lines.push(...rootLines)
315
- })
316
-
317
- return yield* Console.log(lines.join('\n'))
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
- export interface LogPerformanceTraceOptions {
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
- })