@milaboratories/pl-flight-recorder 0.2.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.
- package/README.md +119 -0
- package/dist/analyze.d.ts +133 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +525 -0
- package/dist/analyze.js.map +1 -0
- package/dist/data_summary.d.ts +28 -0
- package/dist/data_summary.d.ts.map +1 -0
- package/dist/data_summary.js +73 -0
- package/dist/data_summary.js.map +1 -0
- package/dist/digest.d.ts +28 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +55 -0
- package/dist/digest.js.map +1 -0
- package/dist/events.d.ts +89 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/instrument.d.ts +82 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +313 -0
- package/dist/instrument.js.map +1 -0
- package/dist/recorder.d.ts +82 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +293 -0
- package/dist/recorder.js.map +1 -0
- package/dist/redact.d.ts +53 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +145 -0
- package/dist/redact.js.map +1 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +377 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +73 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +245 -0
- package/dist/rules.js.map +1 -0
- package/dist/sampler.d.ts +22 -0
- package/dist/sampler.d.ts.map +1 -0
- package/dist/sampler.js +33 -0
- package/dist/sampler.js.map +1 -0
- package/dist/sampler_thread.d.ts +1 -0
- package/dist/sampler_thread.js +41 -0
- package/dist/sampler_thread.js.map +1 -0
- package/dist/session.d.ts +40 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/supervisor.d.ts +58 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +108 -0
- package/dist/supervisor.js.map +1 -0
- package/package.json +43 -0
- package/src/analyze.test.ts +539 -0
- package/src/analyze.ts +795 -0
- package/src/data_summary.ts +110 -0
- package/src/digest.ts +49 -0
- package/src/events.ts +102 -0
- package/src/index.ts +104 -0
- package/src/instrument.ts +442 -0
- package/src/recorder.ts +397 -0
- package/src/redact.test.ts +155 -0
- package/src/redact.ts +213 -0
- package/src/report.ts +512 -0
- package/src/rules.test.ts +182 -0
- package/src/rules.ts +383 -0
- package/src/sampler.ts +40 -0
- package/src/sampler_thread.ts +45 -0
- package/src/session.ts +69 -0
- package/src/supervisor.ts +150 -0
|
@@ -0,0 +1,313 @@
|
|
|
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
|
+
/**
|
|
141
|
+
* Records one block model render.
|
|
142
|
+
*
|
|
143
|
+
* `getStats` exposes the middle layer's own sandbox accounting, whose
|
|
144
|
+
* serialisation byte counts show how much data the model moved across the
|
|
145
|
+
* QuickJS boundary — the model-layer memory cost that no driver call reports.
|
|
146
|
+
*/
|
|
147
|
+
async function recordModelRender(recorder, info, fn) {
|
|
148
|
+
if (!recorder) return await fn();
|
|
149
|
+
const { getStats, ...plain } = info;
|
|
150
|
+
const begin = recorder.event("render-begin", {
|
|
151
|
+
...plain,
|
|
152
|
+
mem: recorder.memorySnapshot()
|
|
153
|
+
});
|
|
154
|
+
const startedAt = performance.now();
|
|
155
|
+
try {
|
|
156
|
+
const result = await fn();
|
|
157
|
+
recorder.event("render-end", {
|
|
158
|
+
begin,
|
|
159
|
+
...plain,
|
|
160
|
+
ms: round(performance.now() - startedAt),
|
|
161
|
+
stats: getStats?.(),
|
|
162
|
+
mem: recorder.memorySnapshot()
|
|
163
|
+
});
|
|
164
|
+
return result;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
recorder.event("render-error", {
|
|
167
|
+
begin,
|
|
168
|
+
...plain,
|
|
169
|
+
ms: round(performance.now() - startedAt),
|
|
170
|
+
error: describeError(error),
|
|
171
|
+
mem: recorder.memorySnapshot()
|
|
172
|
+
});
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Synchronous variant, for a render that is not driven by a promise. */
|
|
177
|
+
function recordModelRenderSync(recorder, info, fn) {
|
|
178
|
+
if (!recorder) return fn();
|
|
179
|
+
const { getStats, ...plain } = info;
|
|
180
|
+
const begin = recorder.event("render-begin", {
|
|
181
|
+
...plain,
|
|
182
|
+
mem: recorder.memorySnapshot()
|
|
183
|
+
});
|
|
184
|
+
const startedAt = performance.now();
|
|
185
|
+
try {
|
|
186
|
+
const result = fn();
|
|
187
|
+
recorder.event("render-end", {
|
|
188
|
+
begin,
|
|
189
|
+
...plain,
|
|
190
|
+
ms: round(performance.now() - startedAt),
|
|
191
|
+
stats: getStats?.(),
|
|
192
|
+
mem: recorder.memorySnapshot()
|
|
193
|
+
});
|
|
194
|
+
return result;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
recorder.event("render-error", {
|
|
197
|
+
begin,
|
|
198
|
+
...plain,
|
|
199
|
+
ms: round(performance.now() - startedAt),
|
|
200
|
+
error: describeError(error),
|
|
201
|
+
mem: recorder.memorySnapshot()
|
|
202
|
+
});
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function record(recorder, registry, handleOf, op, kind, def, call) {
|
|
207
|
+
let digest;
|
|
208
|
+
try {
|
|
209
|
+
digest = digestDef(kind, def);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
digest = { digestFailed: describeError(error) };
|
|
212
|
+
}
|
|
213
|
+
const seq = recorder.event(`${op}-begin`, {
|
|
214
|
+
def: digest,
|
|
215
|
+
mem: recorder.memorySnapshot()
|
|
216
|
+
});
|
|
217
|
+
const startedAt = performance.now();
|
|
218
|
+
let result;
|
|
219
|
+
try {
|
|
220
|
+
result = call();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
recorder.event(`${op}-error`, {
|
|
223
|
+
begin: seq,
|
|
224
|
+
ms: round(performance.now() - startedAt),
|
|
225
|
+
error: describeError(error),
|
|
226
|
+
mem: recorder.memorySnapshot()
|
|
227
|
+
});
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
const handle = handleOf(result);
|
|
231
|
+
registry.put(handle, {
|
|
232
|
+
seq,
|
|
233
|
+
op
|
|
234
|
+
});
|
|
235
|
+
recorder.event(`${op}-end`, {
|
|
236
|
+
begin: seq,
|
|
237
|
+
ms: round(performance.now() - startedAt),
|
|
238
|
+
handle: shortHandle(handle),
|
|
239
|
+
mem: recorder.memorySnapshot()
|
|
240
|
+
});
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
async function span(recorder, op, info, fn) {
|
|
244
|
+
const begin = recorder.event(`${op}-begin`, {
|
|
245
|
+
...info,
|
|
246
|
+
mem: recorder.memorySnapshot()
|
|
247
|
+
});
|
|
248
|
+
const startedAt = performance.now();
|
|
249
|
+
try {
|
|
250
|
+
const { result, detail } = await fn();
|
|
251
|
+
recorder.event(`${op}-end`, {
|
|
252
|
+
begin,
|
|
253
|
+
ms: round(performance.now() - startedAt),
|
|
254
|
+
...detail,
|
|
255
|
+
mem: recorder.memorySnapshot()
|
|
256
|
+
});
|
|
257
|
+
return result;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
recorder.event(`${op}-error`, {
|
|
260
|
+
begin,
|
|
261
|
+
ms: round(performance.now() - startedAt),
|
|
262
|
+
error: describeError(error),
|
|
263
|
+
mem: recorder.memorySnapshot()
|
|
264
|
+
});
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Accepts both a bare handle string and a pool entry wrapping one. */
|
|
269
|
+
function defaultHandleOf(result) {
|
|
270
|
+
if (typeof result === "string") return result;
|
|
271
|
+
const key = result?.key;
|
|
272
|
+
return typeof key === "string" ? key : String(result);
|
|
273
|
+
}
|
|
274
|
+
function vectorsBytes(vectors) {
|
|
275
|
+
let bytes = 0;
|
|
276
|
+
for (const vector of vectors ?? []) {
|
|
277
|
+
const data = vector?.data;
|
|
278
|
+
if (!data) continue;
|
|
279
|
+
const byteLength = data.byteLength;
|
|
280
|
+
if (typeof byteLength === "number") bytes += byteLength;
|
|
281
|
+
else if (Array.isArray(data)) bytes += sampledArrayBytes(data);
|
|
282
|
+
const isNA = vector.isNA;
|
|
283
|
+
if (typeof isNA?.byteLength === "number") bytes += isNA.byteLength;
|
|
284
|
+
}
|
|
285
|
+
return bytes;
|
|
286
|
+
}
|
|
287
|
+
function vectorLength(vector) {
|
|
288
|
+
const data = vector?.data;
|
|
289
|
+
return typeof data?.length === "number" ? data.length : void 0;
|
|
290
|
+
}
|
|
291
|
+
function sampledArrayBytes(data) {
|
|
292
|
+
const sampleSize = Math.min(data.length, 32);
|
|
293
|
+
if (sampleSize === 0) return 0;
|
|
294
|
+
let sampled = 0;
|
|
295
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
296
|
+
const value = data[Math.floor(i * data.length / sampleSize)];
|
|
297
|
+
sampled += typeof value === "string" ? value.length * 2 + 24 : 8;
|
|
298
|
+
}
|
|
299
|
+
return Math.round(sampled / sampleSize * data.length);
|
|
300
|
+
}
|
|
301
|
+
function shortHandle(handle) {
|
|
302
|
+
return typeof handle === "string" ? handle.slice(0, 24) : String(handle);
|
|
303
|
+
}
|
|
304
|
+
function describeError(error) {
|
|
305
|
+
return String(error?.message ?? error);
|
|
306
|
+
}
|
|
307
|
+
function round(value) {
|
|
308
|
+
return Math.round(value * 100) / 100;
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
311
|
+
export { createHandleRegistry, recordModelRender, recordModelRenderSync, wrapDataDriver, wrapModelDriver };
|
|
312
|
+
|
|
313
|
+
//# 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\nexport type 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\nexport type 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 block?: string;\n blockVersion?: 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 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 = 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// Internals\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`, { def: digest, mem: recorder.memorySnapshot() });\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,82 @@
|
|
|
1
|
+
import { FlightRecord, MemorySnapshot } from "./events.js";
|
|
2
|
+
//#region src/recorder.d.ts
|
|
3
|
+
export type RecorderOptions = {
|
|
4
|
+
/** Directory holding flight 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
|
+
/** Memory reading for the calling thread; `rss` is process-wide. */
|
|
25
|
+
memorySnapshot(): MemorySnapshot;
|
|
26
|
+
/** Writes the terminating record. Its absence is how a crash is detected. */
|
|
27
|
+
close(reason?: string): void;
|
|
28
|
+
};
|
|
29
|
+
export type SessionFileInfo = {
|
|
30
|
+
file: string;
|
|
31
|
+
mtimeMs: number;
|
|
32
|
+
bytes: number;
|
|
33
|
+
/** True when the log has no terminating record, i.e. the process died. */
|
|
34
|
+
crashed: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type ParsedSession = {
|
|
37
|
+
file: string;
|
|
38
|
+
records: FlightRecord[];
|
|
39
|
+
/** True when the last line was cut mid-write by the kill. */
|
|
40
|
+
truncatedTail: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Opens a flight log for this process and writes the session header.
|
|
44
|
+
*
|
|
45
|
+
* Records are appended with a synchronous write rather than through a stream:
|
|
46
|
+
* the process being recorded dies without warning — V8 fatal out-of-memory, a
|
|
47
|
+
* failed native allocation, the OS out-of-memory killer — so no exit hook, no
|
|
48
|
+
* flush and no `finally` block runs. Anything still sitting in a userspace
|
|
49
|
+
* buffer is exactly the part that would have explained the death.
|
|
50
|
+
*/
|
|
51
|
+
export declare function openRecorder(options: RecorderOptions): Recorder;
|
|
52
|
+
/**
|
|
53
|
+
* Periodic memory record written by the recorded thread itself.
|
|
54
|
+
*
|
|
55
|
+
* Doubles as a stall detector. This timer cannot fire while its thread is inside
|
|
56
|
+
* a long synchronous call, so a gap here that the independent sampler thread
|
|
57
|
+
* does not share means the thread was blocked — which is also why the heap
|
|
58
|
+
* reading nearest a synchronous blow-up is always stale.
|
|
59
|
+
*/
|
|
60
|
+
export declare function startSelfSampler(recorder: Recorder, intervalMs?: number): () => void;
|
|
61
|
+
/** Flight logs in a directory, newest first, each flagged as crashed or clean. */
|
|
62
|
+
export declare function listSessions(dir: string): SessionFileInfo[];
|
|
63
|
+
/**
|
|
64
|
+
* Parses a flight log, tolerating a final line cut short by a hard kill.
|
|
65
|
+
*
|
|
66
|
+
* A rotated session spans two files: the parked `.1` segment holds the original
|
|
67
|
+
* header and the earlier operations, the active file holds the tail. Both are
|
|
68
|
+
* read so begin records in one segment can be paired with end records in the
|
|
69
|
+
* other; sequence numbers run across the boundary.
|
|
70
|
+
*/
|
|
71
|
+
export declare function readSession(file: string): ParsedSession;
|
|
72
|
+
/**
|
|
73
|
+
* Mints a session id. A parent that supervises a worker calls this, hands the id
|
|
74
|
+
* to the worker, and keeps it for the crash marker, so both sides agree on the
|
|
75
|
+
* session by construction. The leading timestamp is what
|
|
76
|
+
* {@link sessionStartFromId} reads back.
|
|
77
|
+
*/
|
|
78
|
+
export declare function newSessionId(): string;
|
|
79
|
+
/** Session id embedded in a flight log's file name. */
|
|
80
|
+
export declare function sessionIdFromFile(file: string): string;
|
|
81
|
+
//#endregion
|
|
82
|
+
//# 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;;EAE9B,kBAAkB;;EAElB,MAAM;;YAGI;EACV;EACA;EACA;;EAEA;;YAGU;EACV;EACA,SAAS;;EAET;;;;;;;;;;;wBAYc,aAAa,SAAS,kBAAkB;;;;;;;;;wBAmFxC,iBAAiB,UAAU,UAAU;;wBAerC,aAAa,cAAc;;;;;;;;;wBAyB3B,YAAY,eAAe;;;;;;;wBAgC3B;;wBAWA,kBAAkB"}
|