1// @effect-diagnostics nodeBuiltinImport:off - v8.writeHeapSnapshot has no Effect equivalent.
2import * as NodePath from "node:path";
3import * as NodeV8 from "node:v8";
4
5import * as HostProcess from "@t3tools/shared/HostProcess";
6import * as DateTime from "effect/DateTime";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Layer from "effect/Layer";
10
11import * as ServerConfig from "../config.ts";
12
13/**
14 * Writes one V8 heap snapshot into `logsDir` and logs its path. A failed write
15 * logs a warning and removes any partial file, because that file can hold
16 * secrets and the failure is often a full disk.
17 */
18export const writeHeapSnapshot = Effect.fn("server.heapSnapshot", { root: true })(
19 function* (logsDir: string) {
20 const fs = yield* FileSystem.FileSystem;
21 const timestamp = DateTime.formatIso(yield* DateTime.now).replaceAll(":", "-");
22 const path = NodePath.join(logsDir, `server-${process.pid}-${timestamp}.heapsnapshot`);
23 yield* Effect.annotateCurrentSpan({ path });
24 yield* Effect.try(() => NodeV8.writeHeapSnapshot(path)).pipe(
25 Effect.tapError(() => fs.remove(path, { force: true }).pipe(Effect.ignore)),
26 );
27 yield* Effect.logInfo("Wrote heap snapshot.", { path });
28 },
29 Effect.catch((cause) => Effect.logWarning("Failed to write heap snapshot.", { cause })),
30);
31
32/**
33 * Writes a heap snapshot when the process gets SIGUSR2 (`kill -USR2 <pid>`),
34 * so a maintainer can see what a long-running server holds. See "Heap
35 * Snapshots" in docs/operations/observability.md.
36 *
37 * The write blocks the event loop, so two snapshots never overlap: a signal
38 * sent during a write waits until it finishes. Windows has no SIGUSR2, so the
39 * layer does nothing there.
40 */
41export const layer = Layer.effectDiscard(
42 Effect.gen(function* () {
43 if ((yield* HostProcess.Platform) === "win32") return;
44 const { logsDir } = yield* ServerConfig.ServerConfig;
45 const runFork = Effect.runForkWith(yield* Effect.context<FileSystem.FileSystem>());
46 const onSignal = () => void runFork(writeHeapSnapshot(logsDir));
47 yield* Effect.acquireRelease(
48 Effect.sync(() => process.on("SIGUSR2", onSignal)),
49 () => Effect.sync(() => process.off("SIGUSR2", onSignal)),
50 );
51 }),
52);