Skip to content

Commit 46d59f1

Browse files
Allow to interrupt fibers in debugger UI (#80)
1 parent ddbee72 commit 46d59f1

File tree

7 files changed

+204
-28
lines changed

7 files changed

+204
-28
lines changed

.changeset/puny-trains-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect-vscode": minor
3+
---
4+
5+
Allow to interrupt fibers in debugger UI

package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@
112112
"title": "Effect Dev Tools: Reveal Fiber Current Span Location",
113113
"icon": "$(go-to-file)"
114114
},
115+
{
116+
"command": "effect.interruptDebugFiber",
117+
"title": "Effect Dev Tools: Interrupt Fiber",
118+
"icon": "$(debug-stop)"
119+
},
115120
{
116121
"command": "effect.resetTracerExtended",
117122
"title": "Effect Dev Tools: Reset Tracer Extended",
@@ -258,7 +263,12 @@
258263
},
259264
{
260265
"command": "effect.revealFiberCurrentSpan",
261-
"when": "inDebugMode && view === effect-debug-fibers",
266+
"when": "inDebugMode && view === effect-debug-fibers && (viewItem === fiber || viewItem === fiber-interrupting)",
267+
"group": "inline"
268+
},
269+
{
270+
"command": "effect.interruptDebugFiber",
271+
"when": "inDebugMode && view === effect-debug-fibers && viewItem === fiber",
262272
"group": "inline"
263273
}
264274
]

src/DebugEnv.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,22 @@ export class FiberEntry extends Data.Class<{
267267
readonly stack: Array<SpanStackEntry>
268268
readonly isCurrent: boolean
269269
readonly isInterruptible: boolean
270+
readonly isInterrupted: boolean
271+
readonly children: ReadonlyArray<string>
272+
readonly startTimeMillis: number
273+
readonly lifeTimeMillis: number
274+
readonly interrupt: Effect.Effect<void>
270275
}> {
271276
}
272277

273278
const CurrentFiberSchema = Schema.Array(Schema.Struct({
274279
id: Schema.String,
275280
isCurrent: Schema.Boolean,
276-
isInterruptible: Schema.Boolean
281+
isInterrupted: Schema.Boolean,
282+
isInterruptible: Schema.Boolean,
283+
children: Schema.Array(Schema.String),
284+
startTimeMillis: Schema.Number,
285+
lifeTimeMillis: Schema.Number
277286
}))
278287

279288
const getCurrentFibers = (threadId: number | undefined) =>
@@ -291,9 +300,27 @@ const getCurrentFibers = (threadId: number | undefined) =>
291300
Effect.flatMap((fibers) =>
292301
Effect.all(
293302
fibers.map((fiber, idx) =>
294-
Effect.map(
303+
Effect.flatMap(
295304
getFiberCurrentSpan(`(globalThis["effect/devtools/instrumentation"].fibers || [])[${idx}]`, 1, threadId),
296-
(stack) => new FiberEntry({ ...fiber, stack })
305+
(stack) =>
306+
Effect.gen(function*() {
307+
const runtime = yield* Effect.runtime<DebugChannel.DebugChannel>()
308+
309+
return new FiberEntry({
310+
...fiber,
311+
stack,
312+
interrupt: Effect.provide(
313+
DebugChannel.DebugChannel.evaluate({
314+
expression: `globalThis["effect/devtools/instrumentation"].interruptFiber(${
315+
JSON.stringify(fiber.id)
316+
})`,
317+
guessFrameId: true,
318+
threadId
319+
}),
320+
runtime
321+
).pipe(Effect.ignoreLogged)
322+
})
323+
})
297324
)
298325
),
299326
{ concurrency: "unbounded" }

src/DebugFibersProvider.ts

Lines changed: 138 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,47 @@
1-
import type * as Array from "effect/Array"
1+
import * as Array from "effect/Array"
2+
import * as DateTime from "effect/DateTime"
3+
import * as Duration from "effect/Duration"
24
import * as Effect from "effect/Effect"
35
import * as Option from "effect/Option"
46
import * as Stream from "effect/Stream"
57
import * as SubscriptionRef from "effect/SubscriptionRef"
68
import * as vscode from "vscode"
9+
import type * as DebugChannel from "./DebugChannel"
710
import * as Debug from "./DebugEnv"
11+
import * as DurationUtils from "./utils/Duration"
812
import { registerCommand, revealFile, TreeDataProvider, treeDataProvider } from "./VsCode"
913

10-
class TreeNode {
14+
class FiberNode {
15+
readonly _tag = "FiberNode"
16+
public interruptionRequested = false
1117
constructor(readonly entry: Debug.FiberEntry) {}
1218
}
1319

20+
class FiberMetadataNode {
21+
readonly _tag = "FiberMetadataNode"
22+
constructor(readonly name: string, readonly value: string) {}
23+
}
24+
25+
class AttributeNode {
26+
readonly _tag = "AttributeNode"
27+
constructor(readonly name: string, readonly variable: DebugChannel.VariableReference) {}
28+
}
29+
30+
class VariableNode {
31+
readonly _tag = "VariableNode"
32+
constructor(readonly variable: DebugChannel.VariableReference) {}
33+
}
34+
35+
type TreeNode = FiberNode | FiberMetadataNode | AttributeNode | VariableNode
36+
1437
export const DebugFibersProviderLive = treeDataProvider<TreeNode>("effect-debug-fibers")(
1538
(refresh) =>
1639
Effect.gen(function*() {
1740
const debug = yield* Debug.DebugEnv
1841
let nodes: Array<TreeNode> = []
1942

2043
yield* registerCommand("effect.revealFiberCurrentSpan", (node: TreeNode) => {
21-
if (node && node.entry.stack.length > 0) {
44+
if (node && node._tag === "FiberNode" && node.entry.stack.length > 0) {
2245
const stackEntry = node.entry.stack[0]
2346
if (stackEntry.path) {
2447
return revealFile(
@@ -30,15 +53,25 @@ export const DebugFibersProviderLive = treeDataProvider<TreeNode>("effect-debug-
3053
return Effect.void
3154
})
3255

56+
yield* registerCommand("effect.interruptDebugFiber", (node: TreeNode) => {
57+
if (node && node._tag === "FiberNode") {
58+
return node.entry.interrupt.pipe(
59+
Effect.ensuring(Effect.sync(() => node.interruptionRequested = true)),
60+
Effect.ensuring(refresh(Option.some(node)))
61+
)
62+
}
63+
return Effect.void
64+
})
65+
3366
const capture = (threadId?: number) =>
3467
Effect.gen(function*() {
3568
const sessionOption = yield* (SubscriptionRef.get(debug.session))
3669
if (Option.isNone(sessionOption)) {
3770
nodes = []
3871
} else {
3972
const session = sessionOption.value
40-
const pairs = yield* (session.currentFibers(threadId))
41-
nodes = pairs.map((_) => new TreeNode(_))
73+
const rootData = yield* (session.currentFibers(threadId))
74+
nodes = rootData.map((_) => new FiberNode(_))
4275
}
4376
yield* (refresh(Option.none()))
4477
})
@@ -67,7 +100,55 @@ export const DebugFibersProviderLive = treeDataProvider<TreeNode>("effect-debug-
67100
return TreeDataProvider<TreeNode>({
68101
children: Option.match({
69102
onNone: () => Effect.succeedSome(nodes),
70-
onSome: () => Effect.succeedNone
103+
onSome: (node) => {
104+
switch (node._tag) {
105+
case "FiberNode": {
106+
const childs: Array<TreeNode> = [
107+
new FiberMetadataNode(
108+
"Started At",
109+
DateTime.make(node.entry.startTimeMillis).pipe(
110+
Option.map(
111+
DateTime.formatLocal({ dateStyle: "medium", timeStyle: "long" })
112+
),
113+
Option.getOrElse(() => String(node.entry.startTimeMillis))
114+
)
115+
),
116+
new FiberMetadataNode(
117+
"Lifetime",
118+
DurationUtils.format(Duration.millis(node.entry.lifeTimeMillis))
119+
),
120+
new FiberMetadataNode(
121+
"Interruptible",
122+
node.entry.isInterruptible ? "true" : "false"
123+
),
124+
new FiberMetadataNode(
125+
"Interrupted",
126+
node.entry.isInterrupted ? "true" : "false"
127+
),
128+
...node.entry.stack[0]?.attributes.map(([name, variable]) => new AttributeNode(name, variable)) ?? [],
129+
...nodes.filter((_) => _._tag === "FiberNode" && node.entry.children.includes(_.entry.id))
130+
]
131+
return Effect.succeedSome(childs)
132+
}
133+
case "FiberMetadataNode": {
134+
return Effect.succeedNone
135+
}
136+
case "AttributeNode": {
137+
return node.variable.children.pipe(
138+
Effect.map(Array.map((_) => new VariableNode(_))),
139+
Effect.orElseSucceed(() => []),
140+
Effect.asSome
141+
)
142+
}
143+
case "VariableNode": {
144+
return node.variable.children.pipe(
145+
Effect.map(Array.map((_) => new VariableNode(_))),
146+
Effect.orElseSucceed(() => []),
147+
Effect.asSome
148+
)
149+
}
150+
}
151+
}
71152
}),
72153
treeItem: (node) => Effect.succeed(treeItem(node))
73154
})
@@ -77,20 +158,57 @@ export const DebugFibersProviderLive = treeDataProvider<TreeNode>("effect-debug-
77158
// === helpers ===
78159

79160
const treeItem = (node: TreeNode): vscode.TreeItem => {
80-
const item = new vscode.TreeItem(
81-
"Fiber#" + node.entry.id + (node.entry.isInterruptible ? "" : " (uninterruptible)"),
82-
vscode.TreeItemCollapsibleState.None
83-
)
84-
if (node.entry.isCurrent) {
85-
item.iconPath = new vscode.ThemeIcon("arrow-small-right")
86-
}
87-
const firstEntry = node.entry.stack[0]
88-
if (firstEntry) {
89-
item.description = firstEntry.name
90-
if (firstEntry.path) {
91-
item.tooltip = firstEntry.path + ":" + firstEntry.line + ":" + firstEntry.column
161+
switch (node._tag) {
162+
case "FiberNode": {
163+
const item = new vscode.TreeItem(
164+
"Fiber#" + node.entry.id + (node.entry.isInterrupted ? " (interrupting)" : "") +
165+
(node.entry.isInterruptible ? "" : " (uninterruptible)") +
166+
(node.interruptionRequested ? " (interruption requested)" : ""),
167+
vscode.TreeItemCollapsibleState.Collapsed
168+
)
169+
item.contextValue = node.interruptionRequested || node.entry.isInterrupted ? "fiber-interrupting" : "fiber"
170+
if (node.entry.isCurrent) {
171+
item.iconPath = new vscode.ThemeIcon("arrow-small-right")
172+
}
173+
const firstEntry = node.entry.stack[0]
174+
if (firstEntry) {
175+
item.description = firstEntry.name
176+
if (firstEntry.path) {
177+
item.tooltip = firstEntry.path + ":" + firstEntry.line + ":" + firstEntry.column
178+
}
179+
}
180+
181+
return item
182+
}
183+
case "FiberMetadataNode": {
184+
const item = new vscode.TreeItem(node.name, vscode.TreeItemCollapsibleState.None)
185+
item.contextValue = "fiberMetadata"
186+
item.description = node.value
187+
return item
188+
}
189+
case "AttributeNode": {
190+
const item = new vscode.TreeItem(
191+
node.name + ":",
192+
node.variable.isContainer
193+
? vscode.TreeItemCollapsibleState.Collapsed
194+
: vscode.TreeItemCollapsibleState.None
195+
)
196+
item.contextValue = "attribute"
197+
item.description = node.variable.value
198+
item.tooltip = node.variable.value
199+
return item
200+
}
201+
case "VariableNode": {
202+
const item = new vscode.TreeItem(
203+
node.variable.name + ":",
204+
node.variable.isContainer
205+
? vscode.TreeItemCollapsibleState.Collapsed
206+
: vscode.TreeItemCollapsibleState.None
207+
)
208+
item.contextValue = "variable"
209+
item.description = node.variable.value
210+
item.tooltip = node.variable.value
211+
return item
92212
}
93213
}
94-
95-
return item
96214
}

src/instrumentation/instrumentation.compiled.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/instrumentation/instrumentation.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ if (!(instrumentationKey in globalThis)) {
5252
"getFiberCurrentSpanStack": getFiberCurrentSpanStack,
5353
"getFiberCurrentContext": getFiberCurrentContext,
5454
"getAliveFibers": getAliveFibers,
55+
"interruptFiber": interruptFiber,
5556
"getAutoPauseConfig": getAutoPauseConfig,
5657
"togglePauseOnDefects": togglePauseOnDefects,
5758
"getAndUnsetPauseStateToReveal": getAndUnsetPauseStateToReveal
@@ -139,14 +140,29 @@ if (!(instrumentationKey in globalThis)) {
139140
.flatMap((context) => [...context.unsafeMap.entries()])
140141
}
141142

143+
function encodeFiberId(fiber: Fiber.Runtime<any, any>) {
144+
return fiber.id().id.toString()
145+
}
146+
142147
function getAliveFibers() {
143148
return fibers.map((fiber) => ({
144-
"id": fiber.id().id.toString(),
149+
"id": encodeFiberId(fiber),
145150
"isCurrent": fiber === (globalThis as any)["effect/FiberCurrent"],
146-
"isInterruptible": fiber && "currentRuntimeFlags" in fiber && interruptible(fiber.currentRuntimeFlags as any)
151+
"isInterruptible": fiber && "currentRuntimeFlags" in fiber && interruptible(fiber.currentRuntimeFlags as any),
152+
"isInterrupted": fiber && "isInterrupted" in fiber && typeof fiber.isInterrupted === "function" &&
153+
fiber.isInterrupted(),
154+
"children": "getChildren" in fiber && typeof fiber.getChildren === "function"
155+
? [...fiber.getChildren()].map(encodeFiberId)
156+
: [],
157+
"startTimeMillis": fiber.id().startTimeMillis,
158+
"lifeTimeMillis": Date.now() - fiber.id().startTimeMillis
147159
}))
148160
}
149161

162+
function interruptFiber(fiberId: string) {
163+
fibers.forEach((fiber) => encodeFiberId(fiber) === fiberId && fiber.unsafeInterruptAsFork(fiber.id()))
164+
}
165+
150166
function getAutoPauseConfig() {
151167
return {
152168
"pauseOnDefects": debuggerState.pauseOnDefects

tsup.instrumentation.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default defineConfig({
2020
const path = yield* Path.Path
2121
const compiled = yield* fs.readFileString("out/instrumentation.global.js")
2222
const code =
23-
`(function(){ var Array = globalThis.Array; var Object = globalThis.Object; var String = globalThis.String; \n${compiled}} )()`
23+
`(function(){ var Array = globalThis.Array; var Object = globalThis.Object; var String = globalThis.String; var Date = globalThis.Date; \n${compiled}} )()`
2424
yield* fs.writeFileString(
2525
path.join(__dirname, "src", "instrumentation", "instrumentation.compiled.ts"),
2626
`/* eslint-disable @effect/dprint */\nexport const compiledInstrumentationString = ${JSON.stringify(code)}`

0 commit comments

Comments
 (0)