@milaboratories/pl-crash-recorder 0.3.0

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.
Files changed (58) hide show
  1. package/README.md +119 -0
  2. package/dist/data_summary.js +100 -0
  3. package/dist/data_summary.js.map +1 -0
  4. package/dist/digest.js +26 -0
  5. package/dist/digest.js.map +1 -0
  6. package/dist/events.d.ts +149 -0
  7. package/dist/events.d.ts.map +1 -0
  8. package/dist/events.js +13 -0
  9. package/dist/events.js.map +1 -0
  10. package/dist/host_sampler.d.ts +35 -0
  11. package/dist/host_sampler.d.ts.map +1 -0
  12. package/dist/host_sampler.js +54 -0
  13. package/dist/host_sampler.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.js +6 -0
  16. package/dist/instrument.d.ts +79 -0
  17. package/dist/instrument.d.ts.map +1 -0
  18. package/dist/instrument.js +305 -0
  19. package/dist/instrument.js.map +1 -0
  20. package/dist/machine_memory.js +68 -0
  21. package/dist/machine_memory.js.map +1 -0
  22. package/dist/recorder.d.ts +70 -0
  23. package/dist/recorder.d.ts.map +1 -0
  24. package/dist/recorder.js +278 -0
  25. package/dist/recorder.js.map +1 -0
  26. package/dist/redact.js +141 -0
  27. package/dist/redact.js.map +1 -0
  28. package/dist/sampler.d.ts +8 -0
  29. package/dist/sampler.d.ts.map +1 -0
  30. package/dist/sampler.js +33 -0
  31. package/dist/sampler.js.map +1 -0
  32. package/dist/sampler_thread.d.ts +1 -0
  33. package/dist/sampler_thread.js +53 -0
  34. package/dist/sampler_thread.js.map +1 -0
  35. package/dist/session.d.ts +40 -0
  36. package/dist/session.d.ts.map +1 -0
  37. package/dist/session.js +50 -0
  38. package/dist/session.js.map +1 -0
  39. package/dist/supervisor.d.ts +37 -0
  40. package/dist/supervisor.d.ts.map +1 -0
  41. package/dist/supervisor.js +138 -0
  42. package/dist/supervisor.js.map +1 -0
  43. package/package.json +43 -0
  44. package/src/data_summary.ts +163 -0
  45. package/src/digest.ts +36 -0
  46. package/src/events.ts +166 -0
  47. package/src/host_sampler.ts +83 -0
  48. package/src/index.ts +51 -0
  49. package/src/instrument.ts +480 -0
  50. package/src/machine_memory.ts +70 -0
  51. package/src/recorder.test.ts +334 -0
  52. package/src/recorder.ts +435 -0
  53. package/src/redact.test.ts +155 -0
  54. package/src/redact.ts +213 -0
  55. package/src/sampler.ts +40 -0
  56. package/src/sampler_thread.ts +60 -0
  57. package/src/session.ts +71 -0
  58. package/src/supervisor.ts +183 -0
@@ -0,0 +1,305 @@
1
+ import { redact } from "./redact.js";
2
+ import { digestDef } from "./digest.js";
3
+ //#region src/instrument.ts
4
+ /** Maps driver handles back to the join that produced them. */
5
+ function createHandleRegistry(limit = 512) {
6
+ const map = /* @__PURE__ */ new Map();
7
+ return {
8
+ put(handle, origin) {
9
+ if (map.size >= limit) {
10
+ const oldest = map.keys().next();
11
+ if (!oldest.done) map.delete(oldest.value);
12
+ }
13
+ map.set(handle, origin);
14
+ },
15
+ get(handle) {
16
+ return map.get(handle);
17
+ },
18
+ observe(handle, observed) {
19
+ const origin = map.get(handle);
20
+ if (origin) origin.observed = observed;
21
+ }
22
+ };
23
+ }
24
+ /**
25
+ * Wraps the driver that block models call to build frames and tables.
26
+ *
27
+ * Records the redacted join tree and its structural findings, then remembers
28
+ * which handle came from which join so later data calls can be attributed back
29
+ * to the definition that caused them.
30
+ */
31
+ function wrapModelDriver(driver, recorder, registry, handleOf = defaultHandleOf) {
32
+ return Object.assign(Object.create(driver), {
33
+ createPFrame(def) {
34
+ return record(recorder, registry, handleOf, "createPFrame", "PFrameDef", def, () => driver.createPFrame(def));
35
+ },
36
+ createPTable(def) {
37
+ return record(recorder, registry, handleOf, "createPTable", "PTableDef", def, () => driver.createPTable(def));
38
+ },
39
+ createPTableV2(def) {
40
+ return record(recorder, registry, handleOf, "createPTableV2", "PTableDefV2", def, () => driver.createPTableV2(def));
41
+ }
42
+ });
43
+ }
44
+ /**
45
+ * Wraps the asynchronous data-access driver.
46
+ *
47
+ * Adds the observed table shape, the size of what crossed back into JavaScript,
48
+ * and the amplification between input and output rows — the empirical
49
+ * counterpart to the structural findings taken from the join tree.
50
+ */
51
+ function wrapDataDriver(driver, recorder, registry) {
52
+ const source = driver;
53
+ return Object.assign(Object.create(driver), {
54
+ async getShape(handle, ...rest) {
55
+ const origin = registry.get(handle);
56
+ return await span(recorder, "getShape", {
57
+ handle: shortHandle(handle),
58
+ joinSeq: origin?.seq
59
+ }, async () => {
60
+ const shape = await source.getShape(handle, ...rest);
61
+ registry.observe(handle, {
62
+ rows: shape?.rows,
63
+ columns: shape?.columns
64
+ });
65
+ return {
66
+ result: shape,
67
+ detail: {
68
+ rows: shape?.rows,
69
+ columns: shape?.columns
70
+ }
71
+ };
72
+ });
73
+ },
74
+ async getData(handle, columnIndices, range, ...rest) {
75
+ const origin = registry.get(handle);
76
+ return await span(recorder, "getData", {
77
+ handle: shortHandle(handle),
78
+ joinSeq: origin?.seq,
79
+ columnCount: columnIndices?.length,
80
+ range: range ? {
81
+ offset: range.offset,
82
+ length: range.length
83
+ } : null,
84
+ unbounded: !range,
85
+ tableRows: origin?.observed?.rows
86
+ }, async () => {
87
+ const data = await source.getData(handle, columnIndices, range, ...rest);
88
+ return {
89
+ result: data,
90
+ detail: { returnedBytes: vectorsBytes(data) }
91
+ };
92
+ });
93
+ },
94
+ async calculateTableData(handle, request, ...rest) {
95
+ return await span(recorder, "calculateTableData", {
96
+ handle: shortHandle(handle),
97
+ def: digestDef("PTableDef", request)
98
+ }, async () => {
99
+ const data = await source.calculateTableData(handle, request, ...rest);
100
+ const vectors = (data ?? []).map((column) => column?.data);
101
+ return {
102
+ result: data,
103
+ detail: {
104
+ columns: data?.length,
105
+ returnedBytes: vectorsBytes(vectors)
106
+ }
107
+ };
108
+ });
109
+ },
110
+ async getUniqueValues(handle, request, ...rest) {
111
+ return await span(recorder, "getUniqueValues", {
112
+ handle: shortHandle(handle),
113
+ request: redact(request).value
114
+ }, async () => {
115
+ const response = await source.getUniqueValues(handle, request, ...rest);
116
+ return {
117
+ result: response,
118
+ detail: {
119
+ uniqueValues: vectorLength(response?.values),
120
+ overflow: response?.overflow,
121
+ returnedBytes: vectorsBytes([response?.values])
122
+ }
123
+ };
124
+ });
125
+ },
126
+ async findColumns(handle, request, ...rest) {
127
+ return await span(recorder, "findColumns", {
128
+ handle: shortHandle(handle),
129
+ request: redact(request).value
130
+ }, async () => {
131
+ const response = await source.findColumns(handle, request, ...rest);
132
+ return {
133
+ result: response,
134
+ detail: { hits: response?.hits?.length }
135
+ };
136
+ });
137
+ }
138
+ });
139
+ }
140
+ /** Synchronous variant, for a render that is not driven by a promise. */
141
+ function recordModelRenderSync(recorder, info, fn) {
142
+ if (!recorder) return fn();
143
+ const { getStats, block: _b, blockVersion: _v, blockSource: _s, sdkVersion: _k, ...plain } = info;
144
+ announceBlock(recorder, info);
145
+ const begin = recorder.event("render-begin", {
146
+ ...plain,
147
+ mem: recorder.memorySnapshot()
148
+ });
149
+ const startedAt = performance.now();
150
+ openRenders.push(info.blockId);
151
+ try {
152
+ const result = fn();
153
+ recorder.event("render-end", {
154
+ begin,
155
+ ...plain,
156
+ ms: round(performance.now() - startedAt),
157
+ stats: getStats?.(),
158
+ mem: recorder.memorySnapshot()
159
+ });
160
+ return result;
161
+ } catch (error) {
162
+ recorder.event("render-error", {
163
+ begin,
164
+ ...plain,
165
+ ms: round(performance.now() - startedAt),
166
+ error: describeError(error),
167
+ mem: recorder.memorySnapshot()
168
+ });
169
+ throw error;
170
+ } finally {
171
+ openRenders.pop();
172
+ }
173
+ }
174
+ /** Blocks whose renders are open, innermost last. */
175
+ const openRenders = [];
176
+ /**
177
+ * Writes what a block actually is, once per session.
178
+ *
179
+ * A block id is unique to a project, so on its own it names nothing a reader can
180
+ * open: the package, its version and the SDK it was built against are what point
181
+ * at the code that ran. Written as a sticky record, so a session long enough to
182
+ * rotate its log away from the first render does not lose the legend for every
183
+ * id in what survives.
184
+ */
185
+ function announceBlock(recorder, info) {
186
+ const { blockId, block, blockVersion, blockSource, sdkVersion } = info;
187
+ if (blockId === void 0 || block === void 0) return;
188
+ const identity = `${blockId}\u0000${block}\u0000${blockVersion}\u0000${sdkVersion}`;
189
+ recorder.sticky(identity, "block", {
190
+ blockId,
191
+ block,
192
+ blockVersion,
193
+ blockSource,
194
+ sdkVersion
195
+ });
196
+ }
197
+ function record(recorder, registry, handleOf, op, kind, def, call) {
198
+ let digest;
199
+ try {
200
+ digest = digestDef(kind, def);
201
+ } catch (error) {
202
+ digest = { digestFailed: describeError(error) };
203
+ }
204
+ const seq = recorder.event(`${op}-begin`, {
205
+ def: digest,
206
+ blockId: openRenders.at(-1),
207
+ mem: recorder.memorySnapshot()
208
+ });
209
+ const startedAt = performance.now();
210
+ let result;
211
+ try {
212
+ result = call();
213
+ } catch (error) {
214
+ recorder.event(`${op}-error`, {
215
+ begin: seq,
216
+ ms: round(performance.now() - startedAt),
217
+ error: describeError(error),
218
+ mem: recorder.memorySnapshot()
219
+ });
220
+ throw error;
221
+ }
222
+ const handle = handleOf(result);
223
+ registry.put(handle, {
224
+ seq,
225
+ op
226
+ });
227
+ recorder.event(`${op}-end`, {
228
+ begin: seq,
229
+ ms: round(performance.now() - startedAt),
230
+ handle: shortHandle(handle),
231
+ mem: recorder.memorySnapshot()
232
+ });
233
+ return result;
234
+ }
235
+ async function span(recorder, op, info, fn) {
236
+ const begin = recorder.event(`${op}-begin`, {
237
+ ...info,
238
+ mem: recorder.memorySnapshot()
239
+ });
240
+ const startedAt = performance.now();
241
+ try {
242
+ const { result, detail } = await fn();
243
+ recorder.event(`${op}-end`, {
244
+ begin,
245
+ ms: round(performance.now() - startedAt),
246
+ ...detail,
247
+ mem: recorder.memorySnapshot()
248
+ });
249
+ return result;
250
+ } catch (error) {
251
+ recorder.event(`${op}-error`, {
252
+ begin,
253
+ ms: round(performance.now() - startedAt),
254
+ error: describeError(error),
255
+ mem: recorder.memorySnapshot()
256
+ });
257
+ throw error;
258
+ }
259
+ }
260
+ /** Accepts both a bare handle string and a pool entry wrapping one. */
261
+ function defaultHandleOf(result) {
262
+ if (typeof result === "string") return result;
263
+ const key = result?.key;
264
+ return typeof key === "string" ? key : String(result);
265
+ }
266
+ function vectorsBytes(vectors) {
267
+ let bytes = 0;
268
+ for (const vector of vectors ?? []) {
269
+ const data = vector?.data;
270
+ if (!data) continue;
271
+ const byteLength = data.byteLength;
272
+ if (typeof byteLength === "number") bytes += byteLength;
273
+ else if (Array.isArray(data)) bytes += sampledArrayBytes(data);
274
+ const isNA = vector.isNA;
275
+ if (typeof isNA?.byteLength === "number") bytes += isNA.byteLength;
276
+ }
277
+ return bytes;
278
+ }
279
+ function vectorLength(vector) {
280
+ const data = vector?.data;
281
+ return typeof data?.length === "number" ? data.length : void 0;
282
+ }
283
+ function sampledArrayBytes(data) {
284
+ const sampleSize = Math.min(data.length, 32);
285
+ if (sampleSize === 0) return 0;
286
+ let sampled = 0;
287
+ for (let i = 0; i < sampleSize; i++) {
288
+ const value = data[Math.floor(i * data.length / sampleSize)];
289
+ sampled += typeof value === "string" ? value.length * 2 + 24 : 8;
290
+ }
291
+ return Math.round(sampled / sampleSize * data.length);
292
+ }
293
+ function shortHandle(handle) {
294
+ return typeof handle === "string" ? handle.slice(0, 24) : String(handle);
295
+ }
296
+ function describeError(error) {
297
+ return String(error?.message ?? error);
298
+ }
299
+ function round(value) {
300
+ return Math.round(value * 100) / 100;
301
+ }
302
+ //#endregion
303
+ export { createHandleRegistry, recordModelRenderSync, wrapDataDriver, wrapModelDriver };
304
+
305
+ //# sourceMappingURL=instrument.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrument.js","names":[],"sources":["../src/instrument.ts"],"sourcesContent":["import type { PColumn, PTableDef, PTableDefV2 } from \"@milaboratories/pl-model-common\";\nimport { digestDef, type DefDigest, type DefKind } from \"./digest\";\nimport { redact } from \"./redact\";\nimport type { Recorder } from \"./recorder\";\n\n/**\n * Wrappers for the seams the model layer passes through.\n *\n * Every operation writes a begin record and an end record. That pairing is what\n * makes a crash legible: when the process dies mid-operation the end record is\n * missing, so the log names the exact call that was running when memory ran out\n * — the question a post-crash report has to answer.\n *\n * The wrappers are structural rather than tied to one driver interface, because\n * the same three creation methods appear twice with different return types: the\n * model-facing driver hands back a bare handle, the internal one hands back a\n * pool entry.\n */\n\ntype HandleOrigin = {\n /** Sequence number of the record holding the definition. */\n seq: number;\n op: string;\n observed?: { rows?: number; columns?: number };\n};\n\nexport type HandleRegistry = {\n put(handle: string, origin: HandleOrigin): void;\n get(handle: string): HandleOrigin | undefined;\n observe(handle: string, observed: { rows?: number; columns?: number }): void;\n};\n\ntype ModelDriverLike<H> = {\n createPFrame(def: never): H;\n createPTable(def: never): H;\n createPTableV2(def: never): H;\n};\n\nexport type RenderInfo = {\n blockId?: string;\n /** What the block is, as `organization:name` — not the id it has in a project. */\n block?: string;\n blockVersion?: string;\n /** Where the block came from: a registry, a local pack, a dev folder. */\n blockSource?: string;\n /** SDK the block's model was built against. */\n sdkVersion?: string;\n key?: string;\n argsHash?: string;\n /** Which lambda of the block's model is being rendered. */\n lambda?: string;\n /** Nth resumption of a deferred render, counted from one. */\n recalculation?: number;\n /** Read after the render, so sandbox counters cover the whole call. */\n getStats?: () => unknown;\n /** Any further context the call site wants on the record. */\n [key: string]: unknown;\n};\n\n/** Maps driver handles back to the join that produced them. */\nexport function createHandleRegistry(limit = 512): HandleRegistry {\n const map = new Map<string, HandleOrigin>();\n return {\n put(handle, origin) {\n if (map.size >= limit) {\n const oldest = map.keys().next();\n if (!oldest.done) map.delete(oldest.value);\n }\n map.set(handle, origin);\n },\n get(handle) {\n return map.get(handle);\n },\n observe(handle, observed) {\n const origin = map.get(handle);\n if (origin) origin.observed = observed;\n },\n };\n}\n\n/**\n * Wraps the driver that block models call to build frames and tables.\n *\n * Records the redacted join tree and its structural findings, then remembers\n * which handle came from which join so later data calls can be attributed back\n * to the definition that caused them.\n */\nexport function wrapModelDriver<D extends ModelDriverLike<unknown>>(\n driver: D,\n recorder: Recorder,\n registry: HandleRegistry,\n handleOf: (result: unknown) => string = defaultHandleOf,\n): D {\n const wrapped = {\n createPFrame(def: readonly PColumn<unknown>[]) {\n return record(recorder, registry, handleOf, \"createPFrame\", \"PFrameDef\", def, () =>\n (driver.createPFrame as (d: unknown) => unknown)(def),\n );\n },\n createPTable(def: PTableDef<PColumn<unknown>>) {\n return record(recorder, registry, handleOf, \"createPTable\", \"PTableDef\", def, () =>\n (driver.createPTable as (d: unknown) => unknown)(def),\n );\n },\n createPTableV2(def: PTableDefV2<PColumn<unknown>>) {\n return record(recorder, registry, handleOf, \"createPTableV2\", \"PTableDefV2\", def, () =>\n (driver.createPTableV2 as (d: unknown) => unknown)(def),\n );\n },\n };\n // The three creation methods are replaced and everything else is inherited,\n // so the wrapper satisfies whichever driver interface the caller holds.\n return Object.assign(Object.create(driver as object) as D, wrapped);\n}\n\n/**\n * Wraps the asynchronous data-access driver.\n *\n * Adds the observed table shape, the size of what crossed back into JavaScript,\n * and the amplification between input and output rows — the empirical\n * counterpart to the structural findings taken from the join tree.\n */\nexport function wrapDataDriver<D extends object>(\n driver: D,\n recorder: Recorder,\n registry: HandleRegistry,\n): D {\n const source = driver as unknown as {\n getShape(handle: string, ...rest: unknown[]): Promise<{ rows: number; columns: number }>;\n getData(\n handle: string,\n columnIndices: number[],\n range?: { offset: number; length: number },\n ...rest: unknown[]\n ): Promise<unknown[]>;\n calculateTableData(handle: string, request: unknown, ...rest: unknown[]): Promise<unknown[]>;\n getUniqueValues(\n handle: string,\n request: unknown,\n ...rest: unknown[]\n ): Promise<{ values?: { data?: unknown }; overflow?: boolean }>;\n findColumns(\n handle: string,\n request: unknown,\n ...rest: unknown[]\n ): Promise<{ hits?: unknown[] }>;\n };\n\n const wrapped = {\n async getShape(handle: string, ...rest: unknown[]) {\n const origin = registry.get(handle);\n return await span(\n recorder,\n \"getShape\",\n { handle: shortHandle(handle), joinSeq: origin?.seq },\n async () => {\n const shape = await source.getShape(handle, ...rest);\n registry.observe(handle, { rows: shape?.rows, columns: shape?.columns });\n // How much this join amplified is judged in the analyzer, which can\n // read the definition this handle came from without repeating the\n // work here, on the hot path.\n return { result: shape, detail: { rows: shape?.rows, columns: shape?.columns } };\n },\n );\n },\n\n async getData(\n handle: string,\n columnIndices: number[],\n range?: { offset: number; length: number },\n ...rest: unknown[]\n ) {\n const origin = registry.get(handle);\n return await span(\n recorder,\n \"getData\",\n {\n handle: shortHandle(handle),\n joinSeq: origin?.seq,\n columnCount: columnIndices?.length,\n range: range ? { offset: range.offset, length: range.length } : null,\n // A fetch with no range pulls the whole table into the JS heap, which\n // on a large table is an out-of-memory condition by itself.\n unbounded: !range,\n tableRows: origin?.observed?.rows,\n },\n async () => {\n const data = await source.getData(handle, columnIndices, range, ...rest);\n return { result: data, detail: { returnedBytes: vectorsBytes(data) } };\n },\n );\n },\n\n async calculateTableData(handle: string, request: unknown, ...rest: unknown[]) {\n return await span(\n recorder,\n \"calculateTableData\",\n { handle: shortHandle(handle), def: digestDef(\"PTableDef\", request) },\n async () => {\n const data = await source.calculateTableData(handle, request, ...rest);\n const vectors = (data ?? []).map((column) => (column as { data?: unknown })?.data);\n return {\n result: data,\n detail: { columns: data?.length, returnedBytes: vectorsBytes(vectors) },\n };\n },\n );\n },\n\n // Both of these reach the engine as well, and a filter that matches most of\n // a large axis is a plausible place to run out of memory. The request is\n // recorded redacted: axis identity survives, filter values become hashes.\n async getUniqueValues(handle: string, request: unknown, ...rest: unknown[]) {\n return await span(\n recorder,\n \"getUniqueValues\",\n { handle: shortHandle(handle), request: redact(request).value },\n async () => {\n const response = await source.getUniqueValues(handle, request, ...rest);\n return {\n result: response,\n detail: {\n uniqueValues: vectorLength(response?.values),\n overflow: response?.overflow,\n returnedBytes: vectorsBytes([response?.values]),\n },\n };\n },\n );\n },\n\n async findColumns(handle: string, request: unknown, ...rest: unknown[]) {\n return await span(\n recorder,\n \"findColumns\",\n { handle: shortHandle(handle), request: redact(request).value },\n async () => {\n const response = await source.findColumns(handle, request, ...rest);\n return { result: response, detail: { hits: response?.hits?.length } };\n },\n );\n },\n };\n\n return Object.assign(Object.create(driver) as D, wrapped);\n}\n\n/**\n * Records one block model render.\n *\n * `getStats` exposes the middle layer's own sandbox accounting, whose\n * serialisation byte counts show how much data the model moved across the\n * QuickJS boundary — the model-layer memory cost that no driver call reports.\n */\nexport async function recordModelRender<T>(\n recorder: Recorder | undefined,\n info: RenderInfo,\n fn: () => Promise<T>,\n): Promise<T> {\n if (!recorder) return await fn();\n const { getStats, ...plain } = info;\n const begin = recorder.event(\"render-begin\", { ...plain, mem: recorder.memorySnapshot() });\n const startedAt = performance.now();\n try {\n const result = await fn();\n recorder.event(\"render-end\", {\n begin,\n ...plain,\n ms: round(performance.now() - startedAt),\n stats: getStats?.(),\n mem: recorder.memorySnapshot(),\n });\n return result;\n } catch (error) {\n recorder.event(\"render-error\", {\n begin,\n ...plain,\n ms: round(performance.now() - startedAt),\n error: describeError(error),\n mem: recorder.memorySnapshot(),\n });\n throw error;\n }\n}\n\n/** Synchronous variant, for a render that is not driven by a promise. */\nexport function recordModelRenderSync<T>(\n recorder: Recorder | undefined,\n info: RenderInfo,\n fn: () => T,\n): T {\n if (!recorder) return fn();\n // The identity is written once as its own record and referred to by block id\n // thereafter; repeating it on every render would be the same four fields over\n // and over in the log a crash has to fit into.\n const { getStats, block: _b, blockVersion: _v, blockSource: _s, sdkVersion: _k, ...plain } = info;\n announceBlock(recorder, info);\n const begin = recorder.event(\"render-begin\", { ...plain, mem: recorder.memorySnapshot() });\n const startedAt = performance.now();\n // Driver calls the model makes while this render runs belong to this block.\n // Recorded here rather than inferred later: an inference has to survive log\n // rotation and cannot see a call made after the render that caused it returned.\n openRenders.push(info.blockId);\n try {\n const result = fn();\n recorder.event(\"render-end\", {\n begin,\n ...plain,\n ms: round(performance.now() - startedAt),\n stats: getStats?.(),\n mem: recorder.memorySnapshot(),\n });\n return result;\n } catch (error) {\n recorder.event(\"render-error\", {\n begin,\n ...plain,\n ms: round(performance.now() - startedAt),\n error: describeError(error),\n mem: recorder.memorySnapshot(),\n });\n throw error;\n } finally {\n openRenders.pop();\n }\n}\n\n// Internals\n\n/** Blocks whose renders are open, innermost last. */\nconst openRenders: (string | undefined)[] = [];\n\n/**\n * Writes what a block actually is, once per session.\n *\n * A block id is unique to a project, so on its own it names nothing a reader can\n * open: the package, its version and the SDK it was built against are what point\n * at the code that ran. Written as a sticky record, so a session long enough to\n * rotate its log away from the first render does not lose the legend for every\n * id in what survives.\n */\nfunction announceBlock(recorder: Recorder, info: RenderInfo): void {\n const { blockId, block, blockVersion, blockSource, sdkVersion } = info;\n if (blockId === undefined || block === undefined) return;\n const identity = `${blockId}\\u0000${block}\\u0000${blockVersion}\\u0000${sdkVersion}`;\n recorder.sticky(identity, \"block\", { blockId, block, blockVersion, blockSource, sdkVersion });\n}\n\nfunction record<R>(\n recorder: Recorder,\n registry: HandleRegistry,\n handleOf: (result: unknown) => string,\n op: string,\n kind: DefKind,\n def: unknown,\n call: () => R,\n): R {\n let digest: DefDigest | { digestFailed: string };\n try {\n digest = digestDef(kind, def);\n } catch (error) {\n // Diagnostics must never be the reason a join fails to build.\n digest = { digestFailed: describeError(error) };\n }\n // A creation call is synchronous but not free: it hands the definition to the\n // native engine, which can allocate. It gets a begin/end pair like any other\n // operation, so a death inside it is attributed to it and not to the render\n // around it.\n const seq = recorder.event(`${op}-begin`, {\n def: digest,\n blockId: openRenders.at(-1),\n mem: recorder.memorySnapshot(),\n });\n const startedAt = performance.now();\n\n let result: R;\n try {\n result = call();\n } catch (error) {\n recorder.event(`${op}-error`, {\n begin: seq,\n ms: round(performance.now() - startedAt),\n error: describeError(error),\n mem: recorder.memorySnapshot(),\n });\n throw error;\n }\n\n const handle = handleOf(result);\n registry.put(handle, { seq, op });\n recorder.event(`${op}-end`, {\n begin: seq,\n ms: round(performance.now() - startedAt),\n handle: shortHandle(handle),\n mem: recorder.memorySnapshot(),\n });\n return result;\n}\n\nasync function span<T>(\n recorder: Recorder,\n op: string,\n info: Record<string, unknown>,\n fn: () => Promise<{ result: T; detail: Record<string, unknown> }>,\n): Promise<T> {\n const begin = recorder.event(`${op}-begin`, { ...info, mem: recorder.memorySnapshot() });\n const startedAt = performance.now();\n try {\n const { result, detail } = await fn();\n recorder.event(`${op}-end`, {\n begin,\n ms: round(performance.now() - startedAt),\n ...detail,\n mem: recorder.memorySnapshot(),\n });\n return result;\n } catch (error) {\n recorder.event(`${op}-error`, {\n begin,\n ms: round(performance.now() - startedAt),\n error: describeError(error),\n mem: recorder.memorySnapshot(),\n });\n throw error;\n }\n}\n\n/** Accepts both a bare handle string and a pool entry wrapping one. */\nfunction defaultHandleOf(result: unknown): string {\n if (typeof result === \"string\") return result;\n const key = (result as { key?: unknown } | null)?.key;\n return typeof key === \"string\" ? key : String(result);\n}\n\nfunction vectorsBytes(vectors: unknown[]): number {\n let bytes = 0;\n for (const vector of vectors ?? []) {\n const data = (vector as { data?: unknown; isNA?: { byteLength?: number } } | null)?.data;\n if (!data) continue;\n const byteLength = (data as { byteLength?: number }).byteLength;\n if (typeof byteLength === \"number\") {\n bytes += byteLength;\n } else if (Array.isArray(data)) {\n // String and Bytes columns arrive as plain arrays, and those are the ones\n // that actually threaten the JS heap, so they are sampled not skipped.\n bytes += sampledArrayBytes(data);\n }\n const isNA = (vector as { isNA?: { byteLength?: number } }).isNA;\n if (typeof isNA?.byteLength === \"number\") bytes += isNA.byteLength;\n }\n return bytes;\n}\n\nfunction vectorLength(vector: unknown): number | undefined {\n const data = (vector as { data?: { length?: number } } | undefined)?.data;\n return typeof data?.length === \"number\" ? data.length : undefined;\n}\n\nfunction sampledArrayBytes(data: unknown[]): number {\n const sampleSize = Math.min(data.length, 32);\n if (sampleSize === 0) return 0;\n let sampled = 0;\n for (let i = 0; i < sampleSize; i++) {\n const value = data[Math.floor((i * data.length) / sampleSize)];\n sampled += typeof value === \"string\" ? value.length * 2 + 24 : 8;\n }\n return Math.round((sampled / sampleSize) * data.length);\n}\n\nfunction shortHandle(handle: string): string {\n return typeof handle === \"string\" ? handle.slice(0, 24) : String(handle);\n}\n\nfunction describeError(error: unknown): string {\n return String((error as { message?: unknown } | null)?.message ?? error);\n}\n\nfunction round(value: number): number {\n return Math.round(value * 100) / 100;\n}\n"],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import fs from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ //#region src/machine_memory.ts
4
+ /**
5
+ * Reads where the machine's memory currently is.
6
+ *
7
+ * Costs about two milliseconds on macOS and one file read on Linux, which is why
8
+ * the sampler can afford it once a second. It never throws: a reading that cannot
9
+ * be taken says so and the sampler carries on, because a missing number must not
10
+ * cost the resident-size curve it accompanies.
11
+ */
12
+ function readMachineMemory() {
13
+ try {
14
+ if (process.platform === "darwin") return readDarwin();
15
+ if (process.platform === "linux") return readLinux();
16
+ return { unavailable: `not implemented for ${process.platform}` };
17
+ } catch (error) {
18
+ return { unavailable: error instanceof Error ? error.message : String(error) };
19
+ }
20
+ }
21
+ function readDarwin() {
22
+ const stat = execFileSync("vm_stat", {
23
+ encoding: "utf8",
24
+ timeout: 2e3
25
+ });
26
+ const pageSize = Number(/page size of (\d+) bytes/.exec(stat)?.[1] ?? 4096);
27
+ const pages = (label) => {
28
+ const match = new RegExp(`${label}:\\s+(\\d+)\\.`).exec(stat);
29
+ return match ? Number(match[1]) * pageSize : void 0;
30
+ };
31
+ const swap = execFileSync("sysctl", ["-n", "vm.swapusage"], {
32
+ encoding: "utf8",
33
+ timeout: 2e3
34
+ });
35
+ const swapBytes = (label) => {
36
+ const match = new RegExp(`${label} = ([\\d.]+)M`).exec(swap);
37
+ return match ? Number(match[1]) * 1024 * 1024 : void 0;
38
+ };
39
+ return {
40
+ compressedStored: pages("Pages stored in compressor"),
41
+ compressedOccupied: pages("Pages occupied by compressor"),
42
+ swapUsed: swapBytes("used"),
43
+ swapTotal: swapBytes("total"),
44
+ anonymous: pages("Anonymous pages"),
45
+ fileBacked: pages("File-backed pages"),
46
+ wired: pages("Pages wired down")
47
+ };
48
+ }
49
+ function readLinux() {
50
+ const info = fs.readFileSync("/proc/meminfo", "utf8");
51
+ const kb = (label) => {
52
+ const match = new RegExp(`^${label}:\\s+(\\d+) kB`, "m").exec(info);
53
+ return match ? Number(match[1]) * 1024 : void 0;
54
+ };
55
+ const swapTotal = kb("SwapTotal");
56
+ const swapFree = kb("SwapFree");
57
+ return {
58
+ swapTotal,
59
+ swapUsed: swapTotal !== void 0 && swapFree !== void 0 ? swapTotal - swapFree : void 0,
60
+ anonymous: kb("AnonPages"),
61
+ fileBacked: kb("Cached"),
62
+ wired: kb("Unevictable")
63
+ };
64
+ }
65
+ //#endregion
66
+ export { readMachineMemory };
67
+
68
+ //# sourceMappingURL=machine_memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine_memory.js","names":[],"sources":["../src/machine_memory.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport type { MachineMemory } from \"./events\";\n\n/**\n * Reads where the machine's memory currently is.\n *\n * Costs about two milliseconds on macOS and one file read on Linux, which is why\n * the sampler can afford it once a second. It never throws: a reading that cannot\n * be taken says so and the sampler carries on, because a missing number must not\n * cost the resident-size curve it accompanies.\n */\nexport function readMachineMemory(): MachineMemory {\n try {\n if (process.platform === \"darwin\") return readDarwin();\n if (process.platform === \"linux\") return readLinux();\n return { unavailable: `not implemented for ${process.platform}` };\n } catch (error: unknown) {\n return { unavailable: error instanceof Error ? error.message : String(error) };\n }\n}\n\n// Internals\n\nfunction readDarwin(): MachineMemory {\n const stat = execFileSync(\"vm_stat\", { encoding: \"utf8\", timeout: 2000 });\n // The page size is stated in the header and is 16 KiB on Apple silicon against\n // 4 KiB elsewhere, so every count below is meaningless without reading it.\n const pageSize = Number(/page size of (\\d+) bytes/.exec(stat)?.[1] ?? 4096);\n const pages = (label: string): number | undefined => {\n const match = new RegExp(`${label}:\\\\s+(\\\\d+)\\\\.`).exec(stat);\n return match ? Number(match[1]) * pageSize : undefined;\n };\n\n const swap = execFileSync(\"sysctl\", [\"-n\", \"vm.swapusage\"], { encoding: \"utf8\", timeout: 2000 });\n const swapBytes = (label: string): number | undefined => {\n const match = new RegExp(`${label} = ([\\\\d.]+)M`).exec(swap);\n return match ? Number(match[1]) * 1024 * 1024 : undefined;\n };\n\n return {\n // \"Stored\" counts the memory before compression and is what went missing from\n // a process's resident set; \"occupied\" is what it costs the machine now.\n compressedStored: pages(\"Pages stored in compressor\"),\n compressedOccupied: pages(\"Pages occupied by compressor\"),\n swapUsed: swapBytes(\"used\"),\n swapTotal: swapBytes(\"total\"),\n anonymous: pages(\"Anonymous pages\"),\n fileBacked: pages(\"File-backed pages\"),\n wired: pages(\"Pages wired down\"),\n };\n}\n\nfunction readLinux(): MachineMemory {\n const info = fs.readFileSync(\"/proc/meminfo\", \"utf8\");\n const kb = (label: string): number | undefined => {\n const match = new RegExp(`^${label}:\\\\s+(\\\\d+) kB`, \"m\").exec(info);\n return match ? Number(match[1]) * 1024 : undefined;\n };\n const swapTotal = kb(\"SwapTotal\");\n const swapFree = kb(\"SwapFree\");\n return {\n swapTotal,\n swapUsed: swapTotal !== undefined && swapFree !== undefined ? swapTotal - swapFree : undefined,\n anonymous: kb(\"AnonPages\"),\n fileBacked: kb(\"Cached\"),\n // Linux has no compressor of its own; zram, where present, reports as swap.\n wired: kb(\"Unevictable\"),\n };\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,oBAAmC;CACjD,IAAI;EACF,IAAI,QAAQ,aAAa,UAAU,OAAO,WAAW;EACrD,IAAI,QAAQ,aAAa,SAAS,OAAO,UAAU;EACnD,OAAO,EAAE,aAAa,uBAAuB,QAAQ,WAAW;CAClE,SAAS,OAAgB;EACvB,OAAO,EAAE,aAAa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;CAC/E;AACF;AAIA,SAAS,aAA4B;CACnC,MAAM,OAAO,aAAa,WAAW;EAAE,UAAU;EAAQ,SAAS;CAAK,CAAC;CAGxE,MAAM,WAAW,OAAO,2BAA2B,KAAK,IAAI,CAAC,GAAG,MAAM,IAAI;CAC1E,MAAM,SAAS,UAAsC;EACnD,MAAM,QAAQ,IAAI,OAAO,GAAG,MAAM,eAAe,CAAC,CAAC,KAAK,IAAI;EAC5D,OAAO,QAAQ,OAAO,MAAM,EAAE,IAAI,WAAW,KAAA;CAC/C;CAEA,MAAM,OAAO,aAAa,UAAU,CAAC,MAAM,cAAc,GAAG;EAAE,UAAU;EAAQ,SAAS;CAAK,CAAC;CAC/F,MAAM,aAAa,UAAsC;EACvD,MAAM,QAAQ,IAAI,OAAO,GAAG,MAAM,cAAc,CAAC,CAAC,KAAK,IAAI;EAC3D,OAAO,QAAQ,OAAO,MAAM,EAAE,IAAI,OAAO,OAAO,KAAA;CAClD;CAEA,OAAO;EAGL,kBAAkB,MAAM,4BAA4B;EACpD,oBAAoB,MAAM,8BAA8B;EACxD,UAAU,UAAU,MAAM;EAC1B,WAAW,UAAU,OAAO;EAC5B,WAAW,MAAM,iBAAiB;EAClC,YAAY,MAAM,mBAAmB;EACrC,OAAO,MAAM,kBAAkB;CACjC;AACF;AAEA,SAAS,YAA2B;CAClC,MAAM,OAAO,GAAG,aAAa,iBAAiB,MAAM;CACpD,MAAM,MAAM,UAAsC;EAChD,MAAM,QAAQ,IAAI,OAAO,IAAI,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,IAAI;EAClE,OAAO,QAAQ,OAAO,MAAM,EAAE,IAAI,OAAO,KAAA;CAC3C;CACA,MAAM,YAAY,GAAG,WAAW;CAChC,MAAM,WAAW,GAAG,UAAU;CAC9B,OAAO;EACL;EACA,UAAU,cAAc,KAAA,KAAa,aAAa,KAAA,IAAY,YAAY,WAAW,KAAA;EACrF,WAAW,GAAG,WAAW;EACzB,YAAY,GAAG,QAAQ;EAEvB,OAAO,GAAG,aAAa;CACzB;AACF"}
@@ -0,0 +1,70 @@
1
+ import { MemorySnapshot } from "./events.js";
2
+ //#region src/recorder.d.ts
3
+ export type RecorderOptions = {
4
+ /** Directory holding crash logs; created if absent. */
5
+ dir: string;
6
+ /** Which part of the app is recording, e.g. `middle-layer`. */
7
+ role?: string;
8
+ /** Free-form context stored in the session header (app version, project id). */
9
+ meta?: Record<string, unknown>;
10
+ /** Log is rotated past this size so the tail, which explains the crash, survives. */
11
+ maxFileBytes?: number;
12
+ /**
13
+ * Session id assigned by a supervising parent, so the crash marker the parent
14
+ * writes names this session with certainty rather than by inference.
15
+ * Generated when absent.
16
+ */
17
+ sessionId?: string;
18
+ };
19
+ export type Recorder = {
20
+ readonly sessionId: string;
21
+ readonly file: string;
22
+ /** Appends one record and returns its sequence number. Never throws. */
23
+ event(type: string, payload?: Record<string, unknown>): number;
24
+ /**
25
+ * Appends a record that every later segment of the log gets again.
26
+ *
27
+ * For a fact stated once that the rest of the log is unreadable without — what
28
+ * a block id stands for, say. Written once per `key`; a rotation that discards
29
+ * the original rewrites it, so a long session cannot outlive its own legend.
30
+ *
31
+ * Past a cap on how many such facts a session may carry, the record is still
32
+ * written once and marked `notRetained`: it then survives only until the
33
+ * rotation that discards it.
34
+ */
35
+ sticky(key: string, type: string, payload?: Record<string, unknown>): void;
36
+ /** Memory reading for the calling thread; `rss` is process-wide. */
37
+ memorySnapshot(): MemorySnapshot;
38
+ /** Writes the terminating record. Its absence is how a crash is detected. */
39
+ close(reason?: string): void;
40
+ };
41
+ export type SessionFileInfo = {
42
+ file: string;
43
+ mtimeMs: number;
44
+ bytes: number;
45
+ /** True when the log has no terminating record, i.e. the process died. */
46
+ crashed: boolean;
47
+ };
48
+ /**
49
+ * Opens a crash log for this process and writes the session header.
50
+ *
51
+ * Records are appended with a synchronous write rather than through a stream:
52
+ * the process being recorded dies without warning — V8 fatal out-of-memory, a
53
+ * failed native allocation, the OS out-of-memory killer — so no exit hook, no
54
+ * flush and no `finally` block runs. Anything still sitting in a userspace
55
+ * buffer is exactly the part that would have explained the death.
56
+ */
57
+ export declare function openRecorder(options: RecorderOptions): Recorder;
58
+ /** Crash logs in a directory, newest first, each flagged as crashed or clean. */
59
+ export declare function listSessions(dir: string): SessionFileInfo[];
60
+ /**
61
+ * Mints a session id. A parent that supervises a worker calls this, hands the id
62
+ * to the worker, and keeps it for the crash marker, so both sides agree on the
63
+ * session by construction. The leading timestamp is what
64
+ * {@link sessionStartFromId} reads back.
65
+ */
66
+ export declare function newSessionId(): string;
67
+ /** Session id embedded in a crash log's file name. */
68
+ export declare function sessionIdFromFile(file: string): string;
69
+ //#endregion
70
+ //# sourceMappingURL=recorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder.d.ts","names":[],"sources":["../src/recorder.ts"],"mappings":";;YAcY;;EAEV;;EAEA;;EAEA,OAAO;;EAEP;;;;;;EAMA;;YAGU;WACD;WACA;;EAET,MAAM,cAAc,UAAU;;;;;;;;;;;;EAY9B,OAAO,aAAa,cAAc,UAAU;;EAE5C,kBAAkB;;EAElB,MAAM;;YAGI;EACV;EACA;EACA;;EAEA;;;;;;;;;;;wBAmBc,aAAa,SAAS,kBAAkB;;wBAkHxC,aAAa,cAAc;;;;;;;wBAyD3B;;wBAWA,kBAAkB"}