@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
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { SESSION_END_RECORD } from "./events.js";
|
|
2
|
+
import { listSessions, readSession, sessionIdFromFile, sessionStartFromId } from "./recorder.js";
|
|
3
|
+
import { readCrashMarkers } from "./supervisor.js";
|
|
4
|
+
import { inputRowsMax, joinShapes, structuralFindings } from "./rules.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/analyze.ts
|
|
7
|
+
/**
|
|
8
|
+
* Turns a flight log into an attributed cause.
|
|
9
|
+
*
|
|
10
|
+
* Three independent lines of evidence are combined: which operation was still
|
|
11
|
+
* running when the process died (an unmatched begin record), where memory
|
|
12
|
+
* actually went (resident growth across each completed operation, and which
|
|
13
|
+
* region grew — JS heap, off-heap buffers, or native), and what the join tree
|
|
14
|
+
* looked like before any data was touched. Any one alone is suggestive;
|
|
15
|
+
* together they name a specific call in a specific block.
|
|
16
|
+
*/
|
|
17
|
+
const THRESHOLDS = {
|
|
18
|
+
/** Fraction of the heap ceiling above which the heap counts as exhausted. */
|
|
19
|
+
heapPressure: .85,
|
|
20
|
+
nativeGrowthBytes: 512 * 1024 * 1024,
|
|
21
|
+
amplification: 10,
|
|
22
|
+
unboundedRows: 1e6,
|
|
23
|
+
returnedBytes: 256 * 1024 * 1024,
|
|
24
|
+
inlineEntries: 1e6,
|
|
25
|
+
stallMs: 2e3,
|
|
26
|
+
/** Backward tolerance when matching a marker by time, for parent/worker clock drift. */
|
|
27
|
+
clockToleranceMs: 1e3,
|
|
28
|
+
/** How many open sessions are considered as rivals for an unattributed marker. */
|
|
29
|
+
maxRivalSessions: 8,
|
|
30
|
+
/** A machine-memory claim needs the process to actually be large. */
|
|
31
|
+
machineRssShare: .25
|
|
32
|
+
};
|
|
33
|
+
/** Analyzes the newest crashed session in a directory, else the newest session. */
|
|
34
|
+
function analyzeLatest(dir, options = {}) {
|
|
35
|
+
const { preferCrashed = true } = options;
|
|
36
|
+
const sessions = listSessions(dir);
|
|
37
|
+
if (sessions.length === 0) return void 0;
|
|
38
|
+
return analyzeSession(((preferCrashed ? sessions.find((s) => s.crashed) : void 0) ?? sessions[0]).file, dir);
|
|
39
|
+
}
|
|
40
|
+
/** Analyzes one flight log, merging the sibling sampler series when present. */
|
|
41
|
+
function analyzeSession(file, dir = path.dirname(file)) {
|
|
42
|
+
const { records, truncatedTail } = readSession(file);
|
|
43
|
+
const header = records.find((r) => r.type === "session") ?? {};
|
|
44
|
+
const sessionId = sessionIdFromFile(file);
|
|
45
|
+
const samples = readSamples(path.join(dir, `mem-${sessionId}.ndjson`));
|
|
46
|
+
const ended = records.find((r) => r.type === SESSION_END_RECORD);
|
|
47
|
+
const attribution = ended ? {
|
|
48
|
+
marker: void 0,
|
|
49
|
+
ambiguous: false
|
|
50
|
+
} : findCrashMarker(dir, sessionId, records);
|
|
51
|
+
const crashMarker = attribution.marker;
|
|
52
|
+
const memory = analyzeMemory(records, samples, header.env);
|
|
53
|
+
const operations = pairOperations(records);
|
|
54
|
+
const inFlight = operations.filter((op) => !op.end);
|
|
55
|
+
const findings = [
|
|
56
|
+
...classifyCrashMarker(crashMarker),
|
|
57
|
+
...ambiguousMarkerFinding(attribution.ambiguous),
|
|
58
|
+
...classifyMemory(memory, header.env, crashMarker),
|
|
59
|
+
...collectStructural(records),
|
|
60
|
+
...collectEmpirical(records, operations, definitionBySeq(records)),
|
|
61
|
+
...stallFindings(memory)
|
|
62
|
+
].sort(bySeverity);
|
|
63
|
+
return {
|
|
64
|
+
file,
|
|
65
|
+
sessionId,
|
|
66
|
+
crashed: !ended,
|
|
67
|
+
truncatedTail,
|
|
68
|
+
endedReason: ended?.reason,
|
|
69
|
+
crashMarker,
|
|
70
|
+
env: header.env,
|
|
71
|
+
role: header.role,
|
|
72
|
+
meta: header.meta,
|
|
73
|
+
recordCount: records.length,
|
|
74
|
+
rotations: records.filter((r) => r.type === "session" && r.continuation === true).length,
|
|
75
|
+
memory,
|
|
76
|
+
attribution: operations.filter((op) => typeof op.rssDelta === "number").sort((lhs, rhs) => (rhs.rssDelta ?? 0) - (lhs.rssDelta ?? 0)).slice(0, 12),
|
|
77
|
+
inFlight,
|
|
78
|
+
inFlightAtDeath: inFlight.at(-1),
|
|
79
|
+
renders: summarizeRenders(records),
|
|
80
|
+
findings,
|
|
81
|
+
verdict: buildVerdict({
|
|
82
|
+
crashed: !ended,
|
|
83
|
+
memory,
|
|
84
|
+
inFlight,
|
|
85
|
+
findings,
|
|
86
|
+
crashMarker,
|
|
87
|
+
blockOf: enclosingRenders(records)
|
|
88
|
+
}),
|
|
89
|
+
timeline: records.slice(-40).map(compactRecord)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Thousands separators, or `unknown` when the count was never observed. */
|
|
93
|
+
function formatCount(value) {
|
|
94
|
+
return typeof value === "number" ? value.toLocaleString("en-US") : "unknown";
|
|
95
|
+
}
|
|
96
|
+
/** Binary byte units, or `unknown`. */
|
|
97
|
+
function formatBytes(value) {
|
|
98
|
+
if (typeof value !== "number") return "unknown";
|
|
99
|
+
const units = [
|
|
100
|
+
"B",
|
|
101
|
+
"KiB",
|
|
102
|
+
"MiB",
|
|
103
|
+
"GiB",
|
|
104
|
+
"TiB"
|
|
105
|
+
];
|
|
106
|
+
let index = 0;
|
|
107
|
+
let scaled = Math.abs(value);
|
|
108
|
+
while (scaled >= 1024 && index < units.length - 1) {
|
|
109
|
+
scaled /= 1024;
|
|
110
|
+
index++;
|
|
111
|
+
}
|
|
112
|
+
const digits = scaled < 10 && index > 0 ? 1 : 0;
|
|
113
|
+
return `${value < 0 ? "-" : ""}${scaled.toFixed(digits)} ${units[index]}`;
|
|
114
|
+
}
|
|
115
|
+
const CLOCK_TOLERANCE_MS = THRESHOLDS.clockToleranceMs;
|
|
116
|
+
const MAX_RIVAL_SESSIONS = THRESHOLDS.maxRivalSessions;
|
|
117
|
+
const SEVERITY_ORDER = {
|
|
118
|
+
critical: 0,
|
|
119
|
+
high: 1,
|
|
120
|
+
medium: 2,
|
|
121
|
+
low: 3
|
|
122
|
+
};
|
|
123
|
+
const REGION_RULES = [
|
|
124
|
+
"js-heap-exhaustion-confirmed",
|
|
125
|
+
"js-heap-exhaustion",
|
|
126
|
+
"off-heap-buffer-growth",
|
|
127
|
+
"native-allocation-growth",
|
|
128
|
+
"machine-memory-exhausted"
|
|
129
|
+
];
|
|
130
|
+
const CAUSE_RULES = [
|
|
131
|
+
"cross-join",
|
|
132
|
+
"axis-domain-mismatch",
|
|
133
|
+
"join-amplification",
|
|
134
|
+
"unbounded-getData",
|
|
135
|
+
"huge-inline-column"
|
|
136
|
+
];
|
|
137
|
+
/**
|
|
138
|
+
* The marker for a session is the one whose assigned id names it.
|
|
139
|
+
*
|
|
140
|
+
* Failing that, a marker with no id is attributed by time: a session that died
|
|
141
|
+
* stopped writing, so the marker lands at or just after its last record, while a
|
|
142
|
+
* session that survived kept writing past it. That test only separates them when
|
|
143
|
+
* the other sessions actually did keep writing. When two sessions both look
|
|
144
|
+
* dead, no attribution is made at all — an unattributed marker is reported as
|
|
145
|
+
* such, which is honest, where naming the wrong session is not.
|
|
146
|
+
*/
|
|
147
|
+
function findCrashMarker(dir, sessionId, records) {
|
|
148
|
+
const markers = readCrashMarkers(dir);
|
|
149
|
+
const assigned = markers.find((marker) => isAssigned(marker) && marker.sessionId === sessionId);
|
|
150
|
+
if (assigned) return {
|
|
151
|
+
marker: assigned,
|
|
152
|
+
ambiguous: false
|
|
153
|
+
};
|
|
154
|
+
const thisStart = sessionStartFromId(sessionId);
|
|
155
|
+
const lastWall = records.at(-1)?.wall ?? 0;
|
|
156
|
+
const others = openSessionEnds(dir).filter((session) => session.sessionId !== sessionId);
|
|
157
|
+
for (const marker of markers) {
|
|
158
|
+
if (isAssigned(marker)) continue;
|
|
159
|
+
if (marker.wall < Math.max(thisStart, lastWall - CLOCK_TOLERANCE_MS)) continue;
|
|
160
|
+
if (others.filter((session) => marker.wall >= Math.max(session.start, session.lastWall - CLOCK_TOLERANCE_MS)).length > 0) return {
|
|
161
|
+
marker: void 0,
|
|
162
|
+
ambiguous: true
|
|
163
|
+
};
|
|
164
|
+
return {
|
|
165
|
+
marker,
|
|
166
|
+
ambiguous: false
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
marker: void 0,
|
|
171
|
+
ambiguous: false
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* An older recorder put a guess in `sessionId` and labelled it. Such an id is
|
|
176
|
+
* not identity, so it is read back as an unattributed marker.
|
|
177
|
+
*/
|
|
178
|
+
function isAssigned(marker) {
|
|
179
|
+
return marker.sessionId !== void 0 && marker.sessionIdSource !== "guessed";
|
|
180
|
+
}
|
|
181
|
+
/** Last recorded wall clock of every session that has no terminating record. */
|
|
182
|
+
function openSessionEnds(dir) {
|
|
183
|
+
return listSessions(dir).filter((session) => session.crashed).slice(0, MAX_RIVAL_SESSIONS).map((session) => {
|
|
184
|
+
const id = sessionIdFromFile(session.file);
|
|
185
|
+
let lastWall = 0;
|
|
186
|
+
try {
|
|
187
|
+
lastWall = readSession(session.file).records.at(-1)?.wall ?? 0;
|
|
188
|
+
} catch {}
|
|
189
|
+
return {
|
|
190
|
+
sessionId: id,
|
|
191
|
+
start: sessionStartFromId(id),
|
|
192
|
+
lastWall
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function readSamples(file) {
|
|
197
|
+
try {
|
|
198
|
+
return readSession(file).records;
|
|
199
|
+
} catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function analyzeMemory(records, samples, env) {
|
|
204
|
+
const selfSeries = records.filter((record) => record.mem).map((record) => ({
|
|
205
|
+
wall: record.wall,
|
|
206
|
+
...record.mem
|
|
207
|
+
}));
|
|
208
|
+
const rssSeries = samples.length ? samples.map((s) => ({
|
|
209
|
+
wall: s.wall,
|
|
210
|
+
rss: s.rss,
|
|
211
|
+
freeMemory: s.freeMemory
|
|
212
|
+
})) : selfSeries.map((s) => ({
|
|
213
|
+
wall: s.wall,
|
|
214
|
+
rss: s.rss
|
|
215
|
+
}));
|
|
216
|
+
const last = selfSeries.at(-1);
|
|
217
|
+
const lastSample = samples.at(-1);
|
|
218
|
+
const heapLimit = last?.heapLimit ?? env?.heapLimit;
|
|
219
|
+
return {
|
|
220
|
+
samplerPresent: samples.length > 0,
|
|
221
|
+
sampleCount: rssSeries.length,
|
|
222
|
+
rssSeries,
|
|
223
|
+
peakRss: Math.max(0, ...rssSeries.map((s) => s.rss ?? 0)),
|
|
224
|
+
rssAtDeath: lastSample?.rss ?? last?.rss,
|
|
225
|
+
rssGrowth: rssSeries.length ? (rssSeries.at(-1)?.rss ?? 0) - (rssSeries[0].rss ?? 0) : 0,
|
|
226
|
+
heapUsedAtDeath: last?.heapUsed,
|
|
227
|
+
heapLimit,
|
|
228
|
+
heapPressure: last?.heapUsed && heapLimit ? Math.round(last.heapUsed / heapLimit * 100) / 100 : void 0,
|
|
229
|
+
heapGrowth: growth(selfSeries, "heapUsed"),
|
|
230
|
+
externalGrowth: growth(selfSeries, "external"),
|
|
231
|
+
arrayBuffersGrowth: growth(selfSeries, "arrayBuffers"),
|
|
232
|
+
worstStallMs: Math.max(0, ...records.filter((record) => record.type === "mem-self").map((record) => record.stallMs ?? 0)),
|
|
233
|
+
freeMemoryAtDeath: lastSample?.freeMemory,
|
|
234
|
+
totalMemory: lastSample?.totalMemory ?? env?.totalMemory
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function growth(series, key) {
|
|
238
|
+
const values = series.map((entry) => entry[key]).filter((value) => typeof value === "number");
|
|
239
|
+
return values.length ? values[values.length - 1] - values[0] : void 0;
|
|
240
|
+
}
|
|
241
|
+
function pairOperations(records) {
|
|
242
|
+
const begins = /* @__PURE__ */ new Map();
|
|
243
|
+
const beginMemory = /* @__PURE__ */ new Map();
|
|
244
|
+
const operations = [];
|
|
245
|
+
for (const record of records) {
|
|
246
|
+
if (record.type.endsWith("-begin")) {
|
|
247
|
+
if (begins.has(record.seq)) continue;
|
|
248
|
+
const summary = {
|
|
249
|
+
op: record.type.replace(/-begin$/, ""),
|
|
250
|
+
seq: record.seq,
|
|
251
|
+
wall: record.wall,
|
|
252
|
+
info: compactRecord(record)
|
|
253
|
+
};
|
|
254
|
+
begins.set(record.seq, summary);
|
|
255
|
+
if (record.mem) beginMemory.set(record.seq, record.mem);
|
|
256
|
+
operations.push(summary);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!record.type.endsWith("-end") && !record.type.endsWith("-error")) continue;
|
|
260
|
+
const summary = record.begin === void 0 ? void 0 : begins.get(record.begin);
|
|
261
|
+
if (!summary) continue;
|
|
262
|
+
summary.end = compactRecord(record);
|
|
263
|
+
summary.ms = record.ms;
|
|
264
|
+
summary.failed = record.type.endsWith("-error");
|
|
265
|
+
const beginMem = beginMemory.get(summary.seq);
|
|
266
|
+
if (beginMem && record.mem) {
|
|
267
|
+
summary.rssDelta = record.mem.rss - beginMem.rss;
|
|
268
|
+
summary.heapDelta = record.mem.heapUsed - beginMem.heapUsed;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return operations;
|
|
272
|
+
}
|
|
273
|
+
function collectStructural(records) {
|
|
274
|
+
const enclosing = enclosingRenders(records);
|
|
275
|
+
const out = [];
|
|
276
|
+
for (const record of records) for (const finding of structuralFindings(recordedDef(record))) out.push({
|
|
277
|
+
rule: finding.rule,
|
|
278
|
+
severity: finding.severity,
|
|
279
|
+
detail: finding.detail,
|
|
280
|
+
path: finding.path,
|
|
281
|
+
join: finding.join,
|
|
282
|
+
source: record.type,
|
|
283
|
+
seq: record.seq,
|
|
284
|
+
block: record.blockId ?? enclosing.get(record.seq)
|
|
285
|
+
});
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Maps each record's sequence number to the block whose render was open at that
|
|
290
|
+
* point. Driver calls carry no block identity of their own — the driver does not
|
|
291
|
+
* know which model asked — so the enclosing render span supplies it.
|
|
292
|
+
*/
|
|
293
|
+
function enclosingRenders(records) {
|
|
294
|
+
const out = /* @__PURE__ */ new Map();
|
|
295
|
+
const open = [];
|
|
296
|
+
for (const record of records) {
|
|
297
|
+
if (record.type === "render-begin") {
|
|
298
|
+
if (!open.some((entry) => entry.seq === record.seq)) open.push({
|
|
299
|
+
seq: record.seq,
|
|
300
|
+
blockId: record.blockId
|
|
301
|
+
});
|
|
302
|
+
} else if (record.type === "render-end" || record.type === "render-error") {
|
|
303
|
+
const index = open.findIndex((entry) => entry.seq === record.begin);
|
|
304
|
+
if (index >= 0) open.splice(index, 1);
|
|
305
|
+
}
|
|
306
|
+
const innermost = open.at(-1);
|
|
307
|
+
if (innermost?.blockId) out.set(record.seq, innermost.blockId);
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
function collectEmpirical(records, operations, definitions) {
|
|
312
|
+
const out = [];
|
|
313
|
+
const beginBySeq = new Map(records.map((record) => [record.seq, record]));
|
|
314
|
+
for (const record of records) {
|
|
315
|
+
if (record.type === "getShape-end") {
|
|
316
|
+
const joinSeq = (record.begin === void 0 ? void 0 : beginBySeq.get(record.begin))?.joinSeq;
|
|
317
|
+
const declared = joinSeq === void 0 ? void 0 : inputRowsMax(definitions.get(joinSeq));
|
|
318
|
+
const rows = record.rows;
|
|
319
|
+
const amplification = typeof rows === "number" && typeof declared === "number" && declared > 0 ? Math.round(rows / declared * 100) / 100 : void 0;
|
|
320
|
+
if ((amplification ?? 0) >= THRESHOLDS.amplification) out.push({
|
|
321
|
+
rule: "join-amplification",
|
|
322
|
+
severity: "critical",
|
|
323
|
+
seq: record.seq,
|
|
324
|
+
detail: `join produced ${formatCount(rows)} rows from at most ${formatCount(declared)} declared input rows (x${amplification})`
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const tableRows = record.tableRows ?? 0;
|
|
328
|
+
if (record.type === "getData-begin" && record.unbounded && tableRows > THRESHOLDS.unboundedRows) out.push({
|
|
329
|
+
rule: "unbounded-getData",
|
|
330
|
+
severity: "critical",
|
|
331
|
+
seq: record.seq,
|
|
332
|
+
detail: `getData with no row range on a ${formatCount(tableRows)}-row table pulls the whole table into the JS heap`
|
|
333
|
+
});
|
|
334
|
+
const returnedBytes = record.returnedBytes ?? 0;
|
|
335
|
+
if (record.type === "getData-end" && returnedBytes >= THRESHOLDS.returnedBytes) out.push({
|
|
336
|
+
rule: "large-getData-result",
|
|
337
|
+
severity: "high",
|
|
338
|
+
seq: record.seq,
|
|
339
|
+
detail: `${formatBytes(returnedBytes)} of column data returned into JS in one call`
|
|
340
|
+
});
|
|
341
|
+
if (record.type.startsWith("createP")) for (const inline of inlineColumns(recordedDef(record))) {
|
|
342
|
+
if ((inline.entries ?? 0) < THRESHOLDS.inlineEntries) continue;
|
|
343
|
+
out.push({
|
|
344
|
+
rule: "huge-inline-column",
|
|
345
|
+
severity: "high",
|
|
346
|
+
seq: record.seq,
|
|
347
|
+
detail: `model passed an inline column of ${formatCount(inline.entries)} entries (~${formatBytes(inline.approxBytes)}) through the sandbox`
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
for (const op of operations) {
|
|
352
|
+
if ((op.rssDelta ?? 0) < THRESHOLDS.nativeGrowthBytes) continue;
|
|
353
|
+
out.push({
|
|
354
|
+
rule: "operation-memory-spike",
|
|
355
|
+
severity: "high",
|
|
356
|
+
seq: op.seq,
|
|
357
|
+
detail: `${op.op} grew RSS by ${formatBytes(op.rssDelta)} (heap ${formatBytes(op.heapDelta ?? 0)})`
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
function classifyCrashMarker(marker) {
|
|
363
|
+
if (!marker) return [];
|
|
364
|
+
const explanation = {
|
|
365
|
+
"js-heap-out-of-memory": "the supervisor received ERR_WORKER_OUT_OF_MEMORY: the middle-layer thread exceeded its V8 heap limit",
|
|
366
|
+
"abort-or-fatal-allocation-failure": "the process aborted on a fatal allocation failure (V8 fatal out-of-memory, or a failed native allocation)",
|
|
367
|
+
"killed-by-os": "the OS killed the process (SIGKILL), which is what an out-of-memory kill looks like"
|
|
368
|
+
};
|
|
369
|
+
const firstLine = marker.message ? marker.message.split("\n")[0] : "";
|
|
370
|
+
return [{
|
|
371
|
+
rule: marker.reason === "js-heap-out-of-memory" ? "js-heap-exhaustion-confirmed" : `crash-${marker.reason}`,
|
|
372
|
+
severity: "critical",
|
|
373
|
+
source: "supervisor",
|
|
374
|
+
detail: `${explanation[marker.reason] ?? marker.reason}${firstLine ? ` — ${firstLine}` : ""}`
|
|
375
|
+
}];
|
|
376
|
+
}
|
|
377
|
+
function ambiguousMarkerFinding(ambiguous) {
|
|
378
|
+
if (!ambiguous) return [];
|
|
379
|
+
return [{
|
|
380
|
+
rule: "unattributed-crash-marker",
|
|
381
|
+
severity: "medium",
|
|
382
|
+
detail: "a crash marker sits in this session's time window, but another session in the same directory also stopped writing around then, so it is not attributed to either — spawn the worker with an assigned session id to remove the ambiguity"
|
|
383
|
+
}];
|
|
384
|
+
}
|
|
385
|
+
function classifyMemory(memory, env, crashMarker) {
|
|
386
|
+
const out = [];
|
|
387
|
+
const rssShare = memory.totalMemory ? (memory.rssAtDeath ?? 0) / memory.totalMemory : 0;
|
|
388
|
+
if (memory.freeMemoryAtDeath !== void 0 && memory.totalMemory && memory.freeMemoryAtDeath < memory.totalMemory * .03 && rssShare > THRESHOLDS.machineRssShare) out.push({
|
|
389
|
+
rule: "machine-memory-exhausted",
|
|
390
|
+
severity: "critical",
|
|
391
|
+
detail: `process held ${formatBytes(memory.rssAtDeath)} (${Math.round(rssShare * 100)}%) of ${formatBytes(memory.totalMemory)} with ${formatBytes(memory.freeMemoryAtDeath)} free — the OS, not V8, ended the process`
|
|
392
|
+
});
|
|
393
|
+
if (!crashMarker && memory.heapPressure !== void 0 && memory.heapPressure < THRESHOLDS.heapPressure && memory.worstStallMs >= THRESHOLDS.stallMs) out.push({
|
|
394
|
+
rule: "heap-reading-stale",
|
|
395
|
+
severity: "medium",
|
|
396
|
+
detail: `last JS heap reading is ${formatBytes(memory.heapUsedAtDeath)} but the thread was blocked for ${Math.round(memory.worstStallMs)}ms before the log ends, so the heap was never sampled near the crash`
|
|
397
|
+
});
|
|
398
|
+
if ((memory.heapPressure ?? 0) >= THRESHOLDS.heapPressure) {
|
|
399
|
+
const flag = env?.maxOldSpaceSize ? ` (--max-old-space-size=${env.maxOldSpaceSize})` : "";
|
|
400
|
+
out.push({
|
|
401
|
+
rule: "js-heap-exhaustion",
|
|
402
|
+
severity: "critical",
|
|
403
|
+
detail: `JS heap at ${Math.round((memory.heapPressure ?? 0) * 100)}% of its ${formatBytes(memory.heapLimit)} limit${flag}`
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const offHeap = (memory.externalGrowth ?? 0) + (memory.arrayBuffersGrowth ?? 0);
|
|
407
|
+
const heapGrowth = memory.heapGrowth ?? 0;
|
|
408
|
+
if (memory.rssGrowth >= THRESHOLDS.nativeGrowthBytes && heapGrowth < memory.rssGrowth / 4) {
|
|
409
|
+
const offHeapDominant = offHeap >= THRESHOLDS.nativeGrowthBytes;
|
|
410
|
+
out.push({
|
|
411
|
+
rule: offHeapDominant ? "off-heap-buffer-growth" : "native-allocation-growth",
|
|
412
|
+
severity: "critical",
|
|
413
|
+
detail: offHeapDominant ? `RSS grew ${formatBytes(memory.rssGrowth)} while the JS heap grew ${formatBytes(heapGrowth)}; off-heap ArrayBuffer/external allocation grew ${formatBytes(offHeap)} — the growth is buffers handed out by the pframes engine, not JavaScript objects. Raising --max-old-space-size will not help.` : `RSS grew ${formatBytes(memory.rssGrowth)} while the JS heap grew only ${formatBytes(heapGrowth)} — the allocation is native (pframes engine / Arrow buffers), not JavaScript. Raising --max-old-space-size will not help.`
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
function stallFindings(memory) {
|
|
419
|
+
if (memory.worstStallMs < THRESHOLDS.stallMs) return [];
|
|
420
|
+
return [{
|
|
421
|
+
rule: "event-loop-stall",
|
|
422
|
+
severity: "medium",
|
|
423
|
+
detail: `the recorded thread was blocked for ${Math.round(memory.worstStallMs)}ms — synchronous work (model evaluation, or a blocking native call)`
|
|
424
|
+
}];
|
|
425
|
+
}
|
|
426
|
+
function summarizeRenders(records) {
|
|
427
|
+
const open = /* @__PURE__ */ new Map();
|
|
428
|
+
const out = [];
|
|
429
|
+
for (const record of records) {
|
|
430
|
+
if (record.type === "render-begin") {
|
|
431
|
+
if (open.has(record.seq)) continue;
|
|
432
|
+
const summary = {
|
|
433
|
+
seq: record.seq,
|
|
434
|
+
blockId: record.blockId,
|
|
435
|
+
block: record.block,
|
|
436
|
+
key: record.key
|
|
437
|
+
};
|
|
438
|
+
open.set(record.seq, summary);
|
|
439
|
+
out.push(summary);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (record.type !== "render-end" && record.type !== "render-error") continue;
|
|
443
|
+
const summary = record.begin === void 0 ? void 0 : open.get(record.begin);
|
|
444
|
+
if (!summary) continue;
|
|
445
|
+
summary.end = true;
|
|
446
|
+
summary.ms = record.ms;
|
|
447
|
+
summary.stats = record.stats;
|
|
448
|
+
summary.failed = record.type === "render-error";
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
function buildVerdict(input) {
|
|
453
|
+
const { crashed, memory, inFlight, findings, crashMarker, blockOf } = input;
|
|
454
|
+
const gun = inFlight.at(-1);
|
|
455
|
+
const region = findings.find((finding) => REGION_RULES.includes(finding.rule));
|
|
456
|
+
const cause = findings.find((finding) => CAUSE_RULES.includes(finding.rule));
|
|
457
|
+
const blockId = gun?.info?.blockId ?? (gun === void 0 ? void 0 : blockOf.get(gun.seq)) ?? findings.find((finding) => finding.block)?.block;
|
|
458
|
+
return {
|
|
459
|
+
outcome: crashed ? `session ended without shutdown${crashMarker ? ` — supervisor reported ${crashMarker.reason}` : " (no supervisor marker)"}` : "clean shutdown",
|
|
460
|
+
peakRss: memory.peakRss,
|
|
461
|
+
where: gun ? `${gun.op} started at seq ${gun.seq} and never returned${blockId ? ` (block ${blockId})` : ""}` : "no operation was in flight",
|
|
462
|
+
memoryRegion: region?.rule,
|
|
463
|
+
likelyCause: cause?.rule ?? findings[0]?.rule,
|
|
464
|
+
summary: [
|
|
465
|
+
crashed ? "Process died without running shutdown." : "Session closed normally.",
|
|
466
|
+
gun ? `Last unfinished operation: ${gun.op} (seq ${gun.seq}).` : void 0,
|
|
467
|
+
region?.detail,
|
|
468
|
+
cause ? `Probable cause: ${cause.rule} — ${cause.detail}` : void 0
|
|
469
|
+
].filter(Boolean).join(" ")
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function inlineColumns(def, acc = []) {
|
|
473
|
+
if (!def || typeof def !== "object") return acc;
|
|
474
|
+
const node = def;
|
|
475
|
+
const data = node.data;
|
|
476
|
+
if (data?.kind === "inline") acc.push(data);
|
|
477
|
+
for (const value of Object.values(node)) if (Array.isArray(value)) for (const child of value) inlineColumns(child, acc);
|
|
478
|
+
else if (value && typeof value === "object") inlineColumns(value, acc);
|
|
479
|
+
return acc;
|
|
480
|
+
}
|
|
481
|
+
function compactRecord(record) {
|
|
482
|
+
const { mem, def, ...rest } = record;
|
|
483
|
+
const out = { ...rest };
|
|
484
|
+
if (mem) out.rss = mem.rss;
|
|
485
|
+
if (def) out.defSummary = summarizeDef(def);
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
function summarizeDef(digest) {
|
|
489
|
+
const typed = digest;
|
|
490
|
+
const def = recordedDefFrom(digest);
|
|
491
|
+
const outermost = joinShapes(def)[0];
|
|
492
|
+
return {
|
|
493
|
+
kind: typed?.kind,
|
|
494
|
+
bytes: typed?.redaction?.bytes,
|
|
495
|
+
join: outermost?.join,
|
|
496
|
+
children: outermost?.childCount,
|
|
497
|
+
sharedAxes: outermost?.sharedAxes.length,
|
|
498
|
+
inputRowsMax: outermost?.inputRowsMax ?? inputRowsMax(def),
|
|
499
|
+
rowsUpperBound: outermost?.rowsUpperBound
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** The redacted definition inside a record, if it carries one. */
|
|
503
|
+
function recordedDef(record) {
|
|
504
|
+
return recordedDefFrom(record.def);
|
|
505
|
+
}
|
|
506
|
+
function recordedDefFrom(digest) {
|
|
507
|
+
if (!digest || typeof digest !== "object") return void 0;
|
|
508
|
+
return digest.def;
|
|
509
|
+
}
|
|
510
|
+
/** Definition of each creation call, keyed by the sequence number of its record. */
|
|
511
|
+
function definitionBySeq(records) {
|
|
512
|
+
const out = /* @__PURE__ */ new Map();
|
|
513
|
+
for (const record of records) {
|
|
514
|
+
const def = recordedDef(record);
|
|
515
|
+
if (def !== void 0) out.set(record.seq, def);
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function bySeverity(lhs, rhs) {
|
|
520
|
+
return (SEVERITY_ORDER[lhs.severity] ?? 9) - (SEVERITY_ORDER[rhs.severity] ?? 9);
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
export { THRESHOLDS, analyzeLatest, analyzeSession, formatBytes, formatCount };
|
|
524
|
+
|
|
525
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.js","names":[],"sources":["../src/analyze.ts"],"sourcesContent":["import path from \"node:path\";\nimport type {\n CrashMarker,\n FlightRecord,\n MemorySnapshot,\n SamplerRecord,\n SessionEnvironment,\n} from \"./events\";\nimport { SAMPLER_FILE_PREFIX, SESSION_END_RECORD, SESSION_RECORD } from \"./events\";\nimport { listSessions, readSession, sessionIdFromFile, sessionStartFromId } from \"./recorder\";\nimport { readCrashMarkers } from \"./supervisor\";\nimport { inputRowsMax, joinShapes, structuralFindings, type FindingSeverity } from \"./rules\";\n\n/**\n * Turns a flight log into an attributed cause.\n *\n * Three independent lines of evidence are combined: which operation was still\n * running when the process died (an unmatched begin record), where memory\n * actually went (resident growth across each completed operation, and which\n * region grew — JS heap, off-heap buffers, or native), and what the join tree\n * looked like before any data was touched. Any one alone is suggestive;\n * together they name a specific call in a specific block.\n */\n\nexport const THRESHOLDS = {\n /** Fraction of the heap ceiling above which the heap counts as exhausted. */\n heapPressure: 0.85,\n nativeGrowthBytes: 512 * 1024 * 1024,\n amplification: 10,\n unboundedRows: 1_000_000,\n returnedBytes: 256 * 1024 * 1024,\n inlineEntries: 1_000_000,\n stallMs: 2000,\n /** Backward tolerance when matching a marker by time, for parent/worker clock drift. */\n clockToleranceMs: 1000,\n /** How many open sessions are considered as rivals for an unattributed marker. */\n maxRivalSessions: 8,\n /** A machine-memory claim needs the process to actually be large. */\n machineRssShare: 0.25,\n} as const;\n\nexport type Finding = {\n rule: string;\n severity: FindingSeverity;\n detail: string;\n seq?: number;\n path?: string;\n join?: string;\n source?: string;\n block?: string;\n};\n\nexport type MemoryAnalysis = {\n samplerPresent: boolean;\n sampleCount: number;\n rssSeries: { wall: number; rss?: number; freeMemory?: number }[];\n peakRss: number;\n rssAtDeath?: number;\n rssGrowth: number;\n heapUsedAtDeath?: number;\n heapLimit?: number;\n heapPressure?: number;\n heapGrowth?: number;\n externalGrowth?: number;\n arrayBuffersGrowth?: number;\n worstStallMs: number;\n freeMemoryAtDeath?: number;\n totalMemory?: number;\n};\n\nexport type OperationSummary = {\n op: string;\n seq: number;\n wall: number;\n info: Record<string, unknown>;\n end?: Record<string, unknown>;\n ms?: number;\n failed?: boolean;\n rssDelta?: number;\n heapDelta?: number;\n};\n\nexport type RenderSummary = {\n seq: number;\n blockId?: string;\n block?: string;\n key?: string;\n end?: boolean;\n failed?: boolean;\n ms?: number;\n stats?: { serOutBytes?: number; serInBytes?: number; [key: string]: unknown };\n};\n\nexport type Verdict = {\n outcome: string;\n peakRss: number;\n where: string;\n memoryRegion?: string;\n likelyCause?: string;\n summary: string;\n};\n\nexport type SessionAnalysis = {\n file: string;\n sessionId: string;\n crashed: boolean;\n truncatedTail: boolean;\n endedReason?: string;\n crashMarker?: CrashMarker;\n env?: SessionEnvironment;\n role?: string;\n meta?: Record<string, unknown>;\n recordCount: number;\n /**\n * How many times the log rotated. Each rotation re-emits the session header,\n * so environment and metadata survive, but operations older than the retained\n * segments do not — which is why the count is reported rather than implied.\n */\n rotations: number;\n memory: MemoryAnalysis;\n /** Completed operations ranked by resident growth. */\n attribution: OperationSummary[];\n inFlight: OperationSummary[];\n /** The innermost operation that started and never returned. */\n inFlightAtDeath?: OperationSummary;\n renders: RenderSummary[];\n findings: Finding[];\n verdict: Verdict;\n timeline: Record<string, unknown>[];\n};\n\n/** Analyzes the newest crashed session in a directory, else the newest session. */\nexport function analyzeLatest(\n dir: string,\n options: { preferCrashed?: boolean } = {},\n): SessionAnalysis | undefined {\n const { preferCrashed = true } = options;\n const sessions = listSessions(dir);\n if (sessions.length === 0) return undefined;\n const target = (preferCrashed ? sessions.find((s) => s.crashed) : undefined) ?? sessions[0];\n return analyzeSession(target.file, dir);\n}\n\n/** Analyzes one flight log, merging the sibling sampler series when present. */\nexport function analyzeSession(file: string, dir: string = path.dirname(file)): SessionAnalysis {\n const { records, truncatedTail } = readSession(file);\n const header = (records.find((r) => r.type === SESSION_RECORD) ?? {}) as FlightRecord & {\n env?: SessionEnvironment;\n role?: string;\n meta?: Record<string, unknown>;\n };\n const sessionId = sessionIdFromFile(file);\n const samples = readSamples(path.join(dir, `${SAMPLER_FILE_PREFIX}-${sessionId}.ndjson`));\n\n const ended = records.find((r) => r.type === SESSION_END_RECORD);\n const attribution = ended\n ? { marker: undefined, ambiguous: false }\n : findCrashMarker(dir, sessionId, records);\n const crashMarker = attribution.marker;\n\n const memory = analyzeMemory(records, samples, header.env);\n const operations = pairOperations(records);\n const inFlight = operations.filter((op) => !op.end);\n\n const findings = [\n ...classifyCrashMarker(crashMarker),\n ...ambiguousMarkerFinding(attribution.ambiguous),\n ...classifyMemory(memory, header.env, crashMarker),\n ...collectStructural(records),\n ...collectEmpirical(records, operations, definitionBySeq(records)),\n ...stallFindings(memory),\n ].sort(bySeverity);\n\n return {\n file,\n sessionId,\n crashed: !ended,\n truncatedTail,\n endedReason: ended?.reason as string | undefined,\n crashMarker,\n env: header.env,\n role: header.role,\n meta: header.meta,\n recordCount: records.length,\n rotations: records.filter((r) => r.type === SESSION_RECORD && r.continuation === true).length,\n memory,\n attribution: operations\n .filter((op) => typeof op.rssDelta === \"number\")\n .sort((lhs, rhs) => (rhs.rssDelta ?? 0) - (lhs.rssDelta ?? 0))\n .slice(0, 12),\n inFlight,\n inFlightAtDeath: inFlight.at(-1),\n renders: summarizeRenders(records),\n findings,\n verdict: buildVerdict({\n crashed: !ended,\n memory,\n inFlight,\n findings,\n crashMarker,\n blockOf: enclosingRenders(records),\n }),\n timeline: records.slice(-40).map(compactRecord),\n };\n}\n\n/** Thousands separators, or `unknown` when the count was never observed. */\nexport function formatCount(value: number | undefined): string {\n return typeof value === \"number\" ? value.toLocaleString(\"en-US\") : \"unknown\";\n}\n\n/** Binary byte units, or `unknown`. */\nexport function formatBytes(value: number | undefined): string {\n if (typeof value !== \"number\") return \"unknown\";\n const units = [\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\"];\n let index = 0;\n let scaled = Math.abs(value);\n while (scaled >= 1024 && index < units.length - 1) {\n scaled /= 1024;\n index++;\n }\n const digits = scaled < 10 && index > 0 ? 1 : 0;\n return `${value < 0 ? \"-\" : \"\"}${scaled.toFixed(digits)} ${units[index]}`;\n}\n\n// Internals\n\nconst CLOCK_TOLERANCE_MS = THRESHOLDS.clockToleranceMs;\nconst MAX_RIVAL_SESSIONS = THRESHOLDS.maxRivalSessions;\n\nconst SEVERITY_ORDER: Record<FindingSeverity, number> = {\n critical: 0,\n high: 1,\n medium: 2,\n low: 3,\n};\n\nconst REGION_RULES = [\n \"js-heap-exhaustion-confirmed\",\n \"js-heap-exhaustion\",\n \"off-heap-buffer-growth\",\n \"native-allocation-growth\",\n \"machine-memory-exhausted\",\n];\n\nconst CAUSE_RULES = [\n \"cross-join\",\n \"axis-domain-mismatch\",\n \"join-amplification\",\n \"unbounded-getData\",\n \"huge-inline-column\",\n];\n\n/**\n * The marker for a session is the one whose assigned id names it.\n *\n * Failing that, a marker with no id is attributed by time: a session that died\n * stopped writing, so the marker lands at or just after its last record, while a\n * session that survived kept writing past it. That test only separates them when\n * the other sessions actually did keep writing. When two sessions both look\n * dead, no attribution is made at all — an unattributed marker is reported as\n * such, which is honest, where naming the wrong session is not.\n */\nfunction findCrashMarker(\n dir: string,\n sessionId: string,\n records: FlightRecord[],\n): { marker?: CrashMarker; ambiguous: boolean } {\n const markers = readCrashMarkers(dir);\n const assigned = markers.find((marker) => isAssigned(marker) && marker.sessionId === sessionId);\n if (assigned) return { marker: assigned, ambiguous: false };\n\n const thisStart = sessionStartFromId(sessionId);\n const lastWall = records.at(-1)?.wall ?? 0;\n const others = openSessionEnds(dir).filter((session) => session.sessionId !== sessionId);\n\n for (const marker of markers) {\n if (isAssigned(marker)) continue;\n // A marker cannot predate the session it belongs to; the backward tolerance\n // only absorbs clock drift between the parent and the worker.\n if (marker.wall < Math.max(thisStart, lastWall - CLOCK_TOLERANCE_MS)) continue;\n const rivals = others.filter(\n (session) => marker.wall >= Math.max(session.start, session.lastWall - CLOCK_TOLERANCE_MS),\n );\n if (rivals.length > 0) return { marker: undefined, ambiguous: true };\n return { marker, ambiguous: false };\n }\n return { marker: undefined, ambiguous: false };\n}\n\n/**\n * An older recorder put a guess in `sessionId` and labelled it. Such an id is\n * not identity, so it is read back as an unattributed marker.\n */\nfunction isAssigned(marker: CrashMarker): boolean {\n return marker.sessionId !== undefined && marker.sessionIdSource !== \"guessed\";\n}\n\n/** Last recorded wall clock of every session that has no terminating record. */\nfunction openSessionEnds(dir: string): { sessionId: string; start: number; lastWall: number }[] {\n return listSessions(dir)\n .filter((session) => session.crashed)\n .slice(0, MAX_RIVAL_SESSIONS)\n .map((session) => {\n const id = sessionIdFromFile(session.file);\n let lastWall = 0;\n try {\n lastWall = readSession(session.file).records.at(-1)?.wall ?? 0;\n } catch {\n // A session whose log cannot be read cannot rival anything.\n }\n return { sessionId: id, start: sessionStartFromId(id), lastWall };\n });\n}\n\nfunction readSamples(file: string): SamplerRecord[] {\n try {\n return readSession(file).records as unknown as SamplerRecord[];\n } catch {\n return [];\n }\n}\n\nfunction analyzeMemory(\n records: FlightRecord[],\n samples: SamplerRecord[],\n env: SessionEnvironment | undefined,\n): MemoryAnalysis {\n const selfSeries = records\n .filter((record) => record.mem)\n .map((record) => ({ wall: record.wall, ...record.mem! }));\n const rssSeries = samples.length\n ? samples.map((s) => ({ wall: s.wall, rss: s.rss, freeMemory: s.freeMemory }))\n : selfSeries.map((s) => ({ wall: s.wall, rss: s.rss }));\n\n const last = selfSeries.at(-1);\n const lastSample = samples.at(-1);\n const heapLimit = last?.heapLimit ?? env?.heapLimit;\n\n return {\n samplerPresent: samples.length > 0,\n sampleCount: rssSeries.length,\n rssSeries,\n peakRss: Math.max(0, ...rssSeries.map((s) => s.rss ?? 0)),\n rssAtDeath: lastSample?.rss ?? last?.rss,\n rssGrowth: rssSeries.length ? (rssSeries.at(-1)?.rss ?? 0) - (rssSeries[0].rss ?? 0) : 0,\n heapUsedAtDeath: last?.heapUsed,\n heapLimit,\n heapPressure:\n last?.heapUsed && heapLimit ? Math.round((last.heapUsed / heapLimit) * 100) / 100 : undefined,\n heapGrowth: growth(selfSeries, \"heapUsed\"),\n externalGrowth: growth(selfSeries, \"external\"),\n arrayBuffersGrowth: growth(selfSeries, \"arrayBuffers\"),\n worstStallMs: Math.max(\n 0,\n ...records\n .filter((record) => record.type === \"mem-self\")\n .map((record) => (record.stallMs as number | undefined) ?? 0),\n ),\n freeMemoryAtDeath: lastSample?.freeMemory,\n totalMemory: lastSample?.totalMemory ?? env?.totalMemory,\n };\n}\n\nfunction growth(series: Record<string, number | undefined>[], key: string): number | undefined {\n const values = series\n .map((entry) => entry[key])\n .filter((value): value is number => typeof value === \"number\");\n return values.length ? values[values.length - 1] - values[0] : undefined;\n}\n\n// Pairs each begin record with its end or error by sequence number. Unmatched\n// begins are what was running when the log stopped.\nfunction pairOperations(records: FlightRecord[]): OperationSummary[] {\n const begins = new Map<number, OperationSummary>();\n const beginMemory = new Map<number, MemorySnapshot>();\n const operations: OperationSummary[] = [];\n for (const record of records) {\n if (record.type.endsWith(\"-begin\")) {\n // A begin rewritten into a rotated segment repeats its sequence number;\n // the operation is already known and must not be counted twice.\n if (begins.has(record.seq)) continue;\n const summary: OperationSummary = {\n op: record.type.replace(/-begin$/, \"\"),\n seq: record.seq,\n wall: record.wall,\n info: compactRecord(record),\n };\n begins.set(record.seq, summary);\n if (record.mem) beginMemory.set(record.seq, record.mem);\n operations.push(summary);\n continue;\n }\n if (!record.type.endsWith(\"-end\") && !record.type.endsWith(\"-error\")) continue;\n const summary = record.begin === undefined ? undefined : begins.get(record.begin);\n if (!summary) continue;\n summary.end = compactRecord(record);\n summary.ms = record.ms as number | undefined;\n summary.failed = record.type.endsWith(\"-error\");\n const beginMem = beginMemory.get(summary.seq);\n if (beginMem && record.mem) {\n summary.rssDelta = record.mem.rss - beginMem.rss;\n summary.heapDelta = record.mem.heapUsed - beginMem.heapUsed;\n }\n }\n return operations;\n}\n\nfunction collectStructural(records: FlightRecord[]): Finding[] {\n const enclosing = enclosingRenders(records);\n const out: Finding[] = [];\n for (const record of records) {\n // Rules run here rather than at record time, so they can be revised against\n // logs that already exist and cost nothing on the hot path.\n for (const finding of structuralFindings(recordedDef(record))) {\n out.push({\n rule: finding.rule,\n severity: finding.severity,\n detail: finding.detail,\n path: finding.path,\n join: finding.join,\n source: record.type,\n seq: record.seq,\n block: (record.blockId as string | undefined) ?? enclosing.get(record.seq),\n });\n }\n }\n return out;\n}\n\n/**\n * Maps each record's sequence number to the block whose render was open at that\n * point. Driver calls carry no block identity of their own — the driver does not\n * know which model asked — so the enclosing render span supplies it.\n */\nfunction enclosingRenders(records: FlightRecord[]): Map<number, string> {\n const out = new Map<number, string>();\n const open: { seq: number; blockId?: string }[] = [];\n for (const record of records) {\n if (record.type === \"render-begin\") {\n // A render open across a rotation is written twice with one sequence\n // number: once in the segment that was overwritten, once carried into the\n // new one. Pushing both would leave a copy open after the render returned,\n // and every later driver call would be blamed on a block that had finished.\n if (!open.some((entry) => entry.seq === record.seq)) {\n open.push({ seq: record.seq, blockId: record.blockId as string | undefined });\n }\n } else if (record.type === \"render-end\" || record.type === \"render-error\") {\n const index = open.findIndex((entry) => entry.seq === record.begin);\n if (index >= 0) open.splice(index, 1);\n }\n const innermost = open.at(-1);\n if (innermost?.blockId) out.set(record.seq, innermost.blockId);\n }\n return out;\n}\n\nfunction collectEmpirical(\n records: FlightRecord[],\n operations: OperationSummary[],\n definitions: Map<number, unknown>,\n): Finding[] {\n const out: Finding[] = [];\n const beginBySeq = new Map(records.map((record) => [record.seq, record]));\n for (const record of records) {\n if (record.type === \"getShape-end\") {\n // The observed row count is compared against what the definition of the\n // table declared as input, which is looked up here rather than carried on\n // the record.\n const begin = record.begin === undefined ? undefined : beginBySeq.get(record.begin);\n const joinSeq = begin?.joinSeq as number | undefined;\n const declared = joinSeq === undefined ? undefined : inputRowsMax(definitions.get(joinSeq));\n const rows = record.rows as number | undefined;\n const amplification =\n typeof rows === \"number\" && typeof declared === \"number\" && declared > 0\n ? Math.round((rows / declared) * 100) / 100\n : undefined;\n if ((amplification ?? 0) >= THRESHOLDS.amplification) {\n out.push({\n rule: \"join-amplification\",\n severity: \"critical\",\n seq: record.seq,\n detail: `join produced ${formatCount(rows)} rows from at most ${formatCount(\n declared,\n )} declared input rows (x${amplification})`,\n });\n }\n }\n const tableRows = (record.tableRows as number | undefined) ?? 0;\n if (\n record.type === \"getData-begin\" &&\n record.unbounded &&\n tableRows > THRESHOLDS.unboundedRows\n ) {\n out.push({\n rule: \"unbounded-getData\",\n severity: \"critical\",\n seq: record.seq,\n detail: `getData with no row range on a ${formatCount(tableRows)}-row table pulls the whole table into the JS heap`,\n });\n }\n const returnedBytes = (record.returnedBytes as number | undefined) ?? 0;\n if (record.type === \"getData-end\" && returnedBytes >= THRESHOLDS.returnedBytes) {\n out.push({\n rule: \"large-getData-result\",\n severity: \"high\",\n seq: record.seq,\n detail: `${formatBytes(returnedBytes)} of column data returned into JS in one call`,\n });\n }\n if (record.type.startsWith(\"createP\")) {\n for (const inline of inlineColumns(recordedDef(record))) {\n if ((inline.entries ?? 0) < THRESHOLDS.inlineEntries) continue;\n out.push({\n rule: \"huge-inline-column\",\n severity: \"high\",\n seq: record.seq,\n detail: `model passed an inline column of ${formatCount(inline.entries)} entries (~${formatBytes(\n inline.approxBytes,\n )}) through the sandbox`,\n });\n }\n }\n }\n for (const op of operations) {\n if ((op.rssDelta ?? 0) < THRESHOLDS.nativeGrowthBytes) continue;\n out.push({\n rule: \"operation-memory-spike\",\n severity: \"high\",\n seq: op.seq,\n detail: `${op.op} grew RSS by ${formatBytes(op.rssDelta)} (heap ${formatBytes(op.heapDelta ?? 0)})`,\n });\n }\n return out;\n}\n\nfunction classifyCrashMarker(marker: CrashMarker | undefined): Finding[] {\n if (!marker) return [];\n const explanation: Record<string, string> = {\n \"js-heap-out-of-memory\":\n \"the supervisor received ERR_WORKER_OUT_OF_MEMORY: the middle-layer thread exceeded its V8 heap limit\",\n \"abort-or-fatal-allocation-failure\":\n \"the process aborted on a fatal allocation failure (V8 fatal out-of-memory, or a failed native allocation)\",\n \"killed-by-os\":\n \"the OS killed the process (SIGKILL), which is what an out-of-memory kill looks like\",\n };\n const firstLine = marker.message ? marker.message.split(\"\\n\")[0] : \"\";\n return [\n {\n rule:\n marker.reason === \"js-heap-out-of-memory\"\n ? \"js-heap-exhaustion-confirmed\"\n : `crash-${marker.reason}`,\n severity: \"critical\",\n source: \"supervisor\",\n detail: `${explanation[marker.reason] ?? marker.reason}${firstLine ? ` — ${firstLine}` : \"\"}`,\n },\n ];\n}\n\nfunction ambiguousMarkerFinding(ambiguous: boolean): Finding[] {\n if (!ambiguous) return [];\n return [\n {\n rule: \"unattributed-crash-marker\",\n severity: \"medium\",\n detail:\n \"a crash marker sits in this session's time window, but another session in the same directory also stopped writing around then, so it is not attributed to either — spawn the worker with an assigned session id to remove the ambiguity\",\n },\n ];\n}\n\nfunction classifyMemory(\n memory: MemoryAnalysis,\n env: SessionEnvironment | undefined,\n crashMarker: CrashMarker | undefined,\n): Finding[] {\n const out: Finding[] = [];\n\n // On macOS `os.freemem()` sits near zero at all times because the kernel keeps\n // free pages in the file cache, so a low reading alone means nothing: the\n // process itself has to be large before the OS can plausibly have killed it.\n const rssShare = memory.totalMemory ? (memory.rssAtDeath ?? 0) / memory.totalMemory : 0;\n if (\n memory.freeMemoryAtDeath !== undefined &&\n memory.totalMemory &&\n memory.freeMemoryAtDeath < memory.totalMemory * 0.03 &&\n rssShare > THRESHOLDS.machineRssShare\n ) {\n out.push({\n rule: \"machine-memory-exhausted\",\n severity: \"critical\",\n detail: `process held ${formatBytes(memory.rssAtDeath)} (${Math.round(rssShare * 100)}%) of ${formatBytes(\n memory.totalMemory,\n )} with ${formatBytes(memory.freeMemoryAtDeath)} free — the OS, not V8, ended the process`,\n });\n }\n\n // The last in-thread heap reading predates a synchronous blow-up, so a low\n // reading is not evidence of a healthy heap. Say so rather than conclude.\n if (\n !crashMarker &&\n memory.heapPressure !== undefined &&\n memory.heapPressure < THRESHOLDS.heapPressure &&\n memory.worstStallMs >= THRESHOLDS.stallMs\n ) {\n out.push({\n rule: \"heap-reading-stale\",\n severity: \"medium\",\n detail: `last JS heap reading is ${formatBytes(memory.heapUsedAtDeath)} but the thread was blocked for ${Math.round(\n memory.worstStallMs,\n )}ms before the log ends, so the heap was never sampled near the crash`,\n });\n }\n\n if ((memory.heapPressure ?? 0) >= THRESHOLDS.heapPressure) {\n const flag = env?.maxOldSpaceSize ? ` (--max-old-space-size=${env.maxOldSpaceSize})` : \"\";\n out.push({\n rule: \"js-heap-exhaustion\",\n severity: \"critical\",\n detail: `JS heap at ${Math.round((memory.heapPressure ?? 0) * 100)}% of its ${formatBytes(\n memory.heapLimit,\n )} limit${flag}`,\n });\n }\n\n const offHeap = (memory.externalGrowth ?? 0) + (memory.arrayBuffersGrowth ?? 0);\n const heapGrowth = memory.heapGrowth ?? 0;\n if (memory.rssGrowth >= THRESHOLDS.nativeGrowthBytes && heapGrowth < memory.rssGrowth / 4) {\n const offHeapDominant = offHeap >= THRESHOLDS.nativeGrowthBytes;\n out.push({\n rule: offHeapDominant ? \"off-heap-buffer-growth\" : \"native-allocation-growth\",\n severity: \"critical\",\n detail: offHeapDominant\n ? `RSS grew ${formatBytes(memory.rssGrowth)} while the JS heap grew ${formatBytes(\n heapGrowth,\n )}; off-heap ArrayBuffer/external allocation grew ${formatBytes(offHeap)} — the growth is buffers handed out by the pframes engine, not JavaScript objects. Raising --max-old-space-size will not help.`\n : `RSS grew ${formatBytes(memory.rssGrowth)} while the JS heap grew only ${formatBytes(\n heapGrowth,\n )} — the allocation is native (pframes engine / Arrow buffers), not JavaScript. Raising --max-old-space-size will not help.`,\n });\n }\n return out;\n}\n\nfunction stallFindings(memory: MemoryAnalysis): Finding[] {\n if (memory.worstStallMs < THRESHOLDS.stallMs) return [];\n return [\n {\n rule: \"event-loop-stall\",\n severity: \"medium\",\n detail: `the recorded thread was blocked for ${Math.round(\n memory.worstStallMs,\n )}ms — synchronous work (model evaluation, or a blocking native call)`,\n },\n ];\n}\n\nfunction summarizeRenders(records: FlightRecord[]): RenderSummary[] {\n const open = new Map<number, RenderSummary>();\n const out: RenderSummary[] = [];\n for (const record of records) {\n if (record.type === \"render-begin\") {\n // A render open across a rotation is written twice under one sequence\n // number; the carried copy must not become a second, never-finished render.\n if (open.has(record.seq)) continue;\n const summary: RenderSummary = {\n seq: record.seq,\n blockId: record.blockId as string | undefined,\n block: record.block as string | undefined,\n key: record.key as string | undefined,\n };\n open.set(record.seq, summary);\n out.push(summary);\n continue;\n }\n if (record.type !== \"render-end\" && record.type !== \"render-error\") continue;\n const summary = record.begin === undefined ? undefined : open.get(record.begin);\n if (!summary) continue;\n summary.end = true;\n summary.ms = record.ms as number | undefined;\n summary.stats = record.stats as RenderSummary[\"stats\"];\n summary.failed = record.type === \"render-error\";\n }\n return out;\n}\n\nfunction buildVerdict(input: {\n crashed: boolean;\n memory: MemoryAnalysis;\n inFlight: OperationSummary[];\n findings: Finding[];\n crashMarker?: CrashMarker;\n /** Block whose render was open at each sequence number. */\n blockOf: Map<number, string>;\n}): Verdict {\n const { crashed, memory, inFlight, findings, crashMarker, blockOf } = input;\n const gun = inFlight.at(-1);\n const region = findings.find((finding) => REGION_RULES.includes(finding.rule));\n const cause = findings.find((finding) => CAUSE_RULES.includes(finding.rule));\n\n // A driver call carries no block identity of its own, so it is taken from the\n // render that was open around it rather than from whichever finding happens\n // to have one.\n const blockId =\n (gun?.info?.blockId as string | undefined) ??\n (gun === undefined ? undefined : blockOf.get(gun.seq)) ??\n findings.find((finding) => finding.block)?.block;\n return {\n outcome: crashed\n ? `session ended without shutdown${\n crashMarker ? ` — supervisor reported ${crashMarker.reason}` : \" (no supervisor marker)\"\n }`\n : \"clean shutdown\",\n peakRss: memory.peakRss,\n where: gun\n ? `${gun.op} started at seq ${gun.seq} and never returned${blockId ? ` (block ${blockId})` : \"\"}`\n : \"no operation was in flight\",\n memoryRegion: region?.rule,\n likelyCause: cause?.rule ?? findings[0]?.rule,\n summary: [\n crashed ? \"Process died without running shutdown.\" : \"Session closed normally.\",\n gun ? `Last unfinished operation: ${gun.op} (seq ${gun.seq}).` : undefined,\n region?.detail,\n cause ? `Probable cause: ${cause.rule} — ${cause.detail}` : undefined,\n ]\n .filter(Boolean)\n .join(\" \"),\n };\n}\n\nfunction inlineColumns(\n def: unknown,\n acc: { entries?: number; approxBytes?: number }[] = [],\n): { entries?: number; approxBytes?: number }[] {\n if (!def || typeof def !== \"object\") return acc;\n const node = def as Record<string, unknown>;\n const data = node.data as { kind?: string; entries?: number; approxBytes?: number } | undefined;\n if (data?.kind === \"inline\") acc.push(data);\n for (const value of Object.values(node)) {\n if (Array.isArray(value)) {\n for (const child of value) inlineColumns(child, acc);\n } else if (value && typeof value === \"object\") {\n inlineColumns(value, acc);\n }\n }\n return acc;\n}\n\nfunction compactRecord(record: FlightRecord): Record<string, unknown> {\n const { mem, def, ...rest } = record;\n const out: Record<string, unknown> = { ...rest };\n if (mem) out.rss = mem.rss;\n if (def) out.defSummary = summarizeDef(def);\n return out;\n}\n\nfunction summarizeDef(digest: unknown): Record<string, unknown> {\n const typed = digest as { kind?: string; redaction?: { bytes?: number } } | undefined;\n const def = recordedDefFrom(digest);\n const outermost = joinShapes(def)[0];\n return {\n kind: typed?.kind,\n bytes: typed?.redaction?.bytes,\n join: outermost?.join,\n children: outermost?.childCount,\n sharedAxes: outermost?.sharedAxes.length,\n inputRowsMax: outermost?.inputRowsMax ?? inputRowsMax(def),\n rowsUpperBound: outermost?.rowsUpperBound,\n };\n}\n\n/** The redacted definition inside a record, if it carries one. */\nfunction recordedDef(record: FlightRecord): unknown {\n return recordedDefFrom(record.def);\n}\n\nfunction recordedDefFrom(digest: unknown): unknown {\n if (!digest || typeof digest !== \"object\") return undefined;\n return (digest as { def?: unknown }).def;\n}\n\n/** Definition of each creation call, keyed by the sequence number of its record. */\nfunction definitionBySeq(records: FlightRecord[]): Map<number, unknown> {\n const out = new Map<number, unknown>();\n for (const record of records) {\n const def = recordedDef(record);\n if (def !== undefined) out.set(record.seq, def);\n }\n return out;\n}\n\nfunction bySeverity(lhs: Finding, rhs: Finding): number {\n return (SEVERITY_ORDER[lhs.severity] ?? 9) - (SEVERITY_ORDER[rhs.severity] ?? 9);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/data_summary.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Counts and sizes for a column payload, never the payload.
|
|
4
|
+
*
|
|
5
|
+
* This is the one place that has to know the shape of `DataInfo`, because the
|
|
6
|
+
* numbers that predict a join's cost — rows per partition and their byte sizes —
|
|
7
|
+
* live at type-specific positions inside it. Everything else about a definition
|
|
8
|
+
* is recorded structurally.
|
|
9
|
+
*
|
|
10
|
+
* Chunk statistics are optional: the producing workflow fills them in, so row
|
|
11
|
+
* counts are reported when present and left unknown otherwise rather than
|
|
12
|
+
* guessed.
|
|
13
|
+
*/
|
|
14
|
+
export type DataSummary = {
|
|
15
|
+
kind: string;
|
|
16
|
+
/** Entries for inline or JSON payloads. */
|
|
17
|
+
entries?: number;
|
|
18
|
+
approxBytes?: number;
|
|
19
|
+
keyLength?: number;
|
|
20
|
+
partitionKeyLength?: number;
|
|
21
|
+
parts?: number;
|
|
22
|
+
partsWithStats?: number;
|
|
23
|
+
rows?: number;
|
|
24
|
+
bytes?: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function summarizeData(data: unknown): DataSummary;
|
|
27
|
+
//#endregion
|
|
28
|
+
//# sourceMappingURL=data_summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data_summary.d.ts","names":[],"sources":["../src/data_summary.ts"],"mappings":";;;;;;;;;;;;;YAaY;EACV;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;wBAGc,cAAc,gBAAgB"}
|