Effect.fn stack frames keep the call's arguments alive in forked fibers
I maintainer di solito rispondono entro 1 giorno
Una pull request collegata è già stata integrata.
- #9878 di @Hona — integrata
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 25/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- node.js, typescript
- Ambito
- performance
Direzione di ricerca
Investigate makeFn in packages/effect/src/internal/effect.ts where the stack frame closure captures arguments. Run the provided Node repro to observe retained. A fix moves frame building to a helper outside the traced function; expected outcome is collected and open PR #9878 already implements it.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
What is the problem?
An Effect.fn call's stack frame keeps every argument of that call alive. A fiber forked inside the call inherits the frame through CurrentStackFrame, so the arguments of every traced call above it stay reachable for as long as that fiber lives, long after the calls have returned.
Reproduction
Run with Node against [email protected]:
import { Effect, Fiber } from "effect"
import { setFlagsFromString } from "node:v8"
import { runInNewContext } from "node:vm"
setFlagsFromString("--expose_gc")
const gc = runInNewContext("gc") as () => void
const start = Effect.fn("start")(function*(input: { payload: string }) {
yield* Effect.annotateCurrentSpan("payload", input.payload.length)
return yield* Effect.forkDetach(Effect.never)
})
let reference: WeakRef<object> | undefined
const fiber = await (async () => {
const input = { payload: "x".repeat(1024) }
reference = new WeakRef(input)
return Effect.runPromise(start(input))
})()
for (let i = 0; i < 8; i++) {
await new Promise((resolve) => setTimeout(resolve, 0))
gc()
}
console.log(reference!.deref() ? "retained" : "collected")
await Effect.runPromise(Fiber.interrupt(fiber))
| Result | |
|---|---|
| Expected | collected: start has returned and nothing references input |
| Actual | retained |
Effect.fnUntraced |
collected |
Bun shows the same behaviour.
Cause
makeFn in packages/effect/src/internal/effect.ts builds the frame inside the traced function:
return defineFunctionLength(body.length, function(this: any, ...args: Array<any>) {
let result = suspend(() => {
const iter = body.apply(this, arguments) // closes over `arguments`
...
})
...
return updateService(result, CurrentStackFrame, (prev) => ({
name,
stack: callError ? fnStackCleaner(() => callError.stack) : constUndefined, // same scope
parent: { name: definitionName, stack: definitionStack, parent: prev }
}))
})
The stack closure only reads callError. Engines allocate one context per scope, though, and this scope also holds arguments, which the suspend thunk uses. So the frame's stack closure keeps the whole arguments object alive:
flowchart LR
F["forked fiber"] --> C["context: CurrentStackFrame"]
C --> S["frame.stack closure"]
S --> A["scope: arguments"]
A --> I["every call argument"]
Impact
We found this in a production heap snapshot of a long-running server (opencode). 152 fibers that outlive their calls kept the frame chain … < SessionRunner.runStep, for example:
- watchers and plugin services first started inside a request;
- background job notifiers.
runStep receives the full loaded conversation as its first argument, so those fibers kept 117 MB of message history (including base64 screenshots) alive indefinitely.
Proposed fix
Build the frame in a helper outside the traced function, so the closure's context holds only name, callError and the definition frame. PR: https://github.com/Effect-TS/effect/pull/9878
- Lingua principale
- TypeScript
- Stelle
- 16.7k
- Fork
- 808
- Merge medio
- 11h 31m
- PR unite (30g)
- 834
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di Effect-TS/effect
-
Xml: `positions` decode option that puts `line` and `column` on `StartElement` eventsForse già presa @effect-bot l’ha presa oggi. Aperta
Effect-TS/effect#10400 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
effect/ai: support the MCP Apps extension (`io.modelcontextprotocol/ui`)Forse già presa @effect-bot l’ha presa oggi. Aperta
Effect-TS/effect#10399 · 2 commenti · 1 reazione · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Flat RPC client and AtomRpc query/mutation: type-check cost grows with RPCs in the group × tags calledForse già presa @effect-bot l’ha presa oggi. Aperta
Effect-TS/effect#10396 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Support `format: "password"` as `Redacted` in JSON Schema import/export and the OpenAPI generatorForse già presa @effect-bot l’ha presa oggi. Apertaenhancement
Effect-TS/effect#10394 · 2 commenti · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Versioned schemas with version negotiation in HttpApi, Rpc and clusterForse già presa @effect-bot l’ha presa 1 giorno fa. Aperta
Effect-TS/effect#10383 · 2 commenti · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di Effect-TS/effect
Issue simili
-
test-reliability
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
Azure/azure-sdk-for-js#40672 · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
HarperFast/studio#2067 ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
documentation
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
liberation-data/drivine#639 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno