@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/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @milaboratories/pl-flight-recorder
|
|
2
|
+
|
|
3
|
+
Explains an out-of-memory death in the block model layer after the fact, when
|
|
4
|
+
the process is already gone and the data that caused it cannot be obtained.
|
|
5
|
+
|
|
6
|
+
## Why it is built this way
|
|
7
|
+
|
|
8
|
+
The failure this exists for kills the process in seconds and runs no shutdown
|
|
9
|
+
path: V8 fatal out-of-memory, a failed native allocation, the OS out-of-memory
|
|
10
|
+
killer. That rules out anything that collects on demand from a live process —
|
|
11
|
+
`pprofDump`, cache metrics, a log that flushes on exit — because by the time
|
|
12
|
+
anyone asks, there is nothing to ask.
|
|
13
|
+
|
|
14
|
+
Three consequences shape the design:
|
|
15
|
+
|
|
16
|
+
- **Records are appended synchronously.** A buffered stream loses exactly the
|
|
17
|
+
tail that explains the death. The absence of a terminating `session-end`
|
|
18
|
+
record is how a crash is detected.
|
|
19
|
+
- **Memory is sampled from another thread.** While the middle layer sits inside
|
|
20
|
+
a synchronous pframes call its own timers do not fire, so its memory series
|
|
21
|
+
goes dark precisely when memory is growing fastest.
|
|
22
|
+
- **The parent records the cause.** A thread that runs out of heap cannot
|
|
23
|
+
describe its own death; its last reading predates the blow-up. Node reports
|
|
24
|
+
`ERR_WORKER_OUT_OF_MEMORY` to the parent, which turns a guess into a fact.
|
|
25
|
+
|
|
26
|
+
## Enabling it
|
|
27
|
+
|
|
28
|
+
Inert unless `MI_FLIGHT_RECORDER_DIR` points at a directory. Recording appends
|
|
29
|
+
on every recorded operation and that cost has not yet been measured against a
|
|
30
|
+
real project, so it is opt-in rather than on by default.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
MI_FLIGHT_RECORDER_DIR=~/platforma-flight <start the app>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Reading a log
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { analyzeLatest, renderReport } from "@milaboratories/pl-flight-recorder";
|
|
40
|
+
|
|
41
|
+
const analysis = analyzeLatest(dir); // prefers the session that crashed
|
|
42
|
+
console.log(renderReport(analysis));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The verdict combines three independent lines of evidence, none sufficient alone:
|
|
46
|
+
|
|
47
|
+
1. **What was in flight.** Every operation writes a begin and an end record, so
|
|
48
|
+
an unmatched begin names the exact call that was running when memory ran out.
|
|
49
|
+
2. **Which memory ran out.** JS heap (confirmed by the parent), off-heap
|
|
50
|
+
ArrayBuffers handed out by the engine, native allocation, or the machine.
|
|
51
|
+
This decides whether `--max-old-space-size` is even relevant.
|
|
52
|
+
3. **Why the data was that big.** Structural faults readable from the recorded
|
|
53
|
+
shape before any data is touched — a cartesian join, or axes that agree on
|
|
54
|
+
name and type but differ in domain — plus the observed row count against the
|
|
55
|
+
input the definition declared.
|
|
56
|
+
|
|
57
|
+
## What is recorded, and how
|
|
58
|
+
|
|
59
|
+
A definition is recorded by **shape**, not by a hand-written digest per
|
|
60
|
+
definition type. `redact` walks whatever the seam was handed and rebuilds it:
|
|
61
|
+
keys, numbers and the small set of strings that are schema survive verbatim,
|
|
62
|
+
every other string becomes a hash and a length, and a column payload is replaced
|
|
63
|
+
by counts. The default for an unrecognised string is to hash it, so a field this
|
|
64
|
+
code has never seen can make a report less informative but never make it leak.
|
|
65
|
+
|
|
66
|
+
- Kept: definition shape, column and axis names, value types, axis domains
|
|
67
|
+
(they carry join identity), filter operators, row/byte/partition counts.
|
|
68
|
+
- Hashed: filter reference values, annotation values (`pl7.app/label` carries
|
|
69
|
+
user-entered sample names), column ids, every other string. The hash is
|
|
70
|
+
stable, so two labels can be compared without either being revealed.
|
|
71
|
+
- Dropped: cell values, inline column payloads, partition keys.
|
|
72
|
+
|
|
73
|
+
Two things bound one record: arrays keep their head and carry an `$omitted`
|
|
74
|
+
marker, and a node budget stops a pathological definition from filling the log.
|
|
75
|
+
A session's log is bounded the same way, at two segments of 32 MiB. Rotation
|
|
76
|
+
rewrites into the new segment the three things a report cannot be produced
|
|
77
|
+
without — the session header, the earliest memory reading, and the begin record
|
|
78
|
+
of every operation still open — so repeated rotation costs only operations that
|
|
79
|
+
already completed. The report states how many rotations happened.
|
|
80
|
+
Class instances are named (`$opaque`) rather than walked, which matters because
|
|
81
|
+
a definition can carry live accessors that reference each other.
|
|
82
|
+
|
|
83
|
+
On a realistic definition (two columns, 72 partitions, a 1200-value filter) the
|
|
84
|
+
record is ~1 KB against ~23 KB for the definition as it stands — but size is not
|
|
85
|
+
why this exists. The definition as it stands cannot be sent to us at all.
|
|
86
|
+
|
|
87
|
+
Only one module knows a definition's types: `data_summary`, which reads row
|
|
88
|
+
counts and byte sizes out of `DataInfo` because those numbers live at
|
|
89
|
+
type-specific positions and they are what predicts a join's cost.
|
|
90
|
+
|
|
91
|
+
## Rules run in the analyzer
|
|
92
|
+
|
|
93
|
+
Structural rules are not evaluated while recording. Nothing is computed on the
|
|
94
|
+
hot path, the rules can be revised against logs that already exist, and one
|
|
95
|
+
implementation covers every definition shape: join nodes are recognised by their
|
|
96
|
+
discriminator and their children by position, so the original tree API and the
|
|
97
|
+
V2 query API are read by the same walk.
|
|
98
|
+
|
|
99
|
+
## Where it hooks in
|
|
100
|
+
|
|
101
|
+
- `pl-middle-layer/src/middle_layer/driver_kit.ts` wraps the pFrame driver once.
|
|
102
|
+
Every join a model builds and every row it reads passes through it, so both
|
|
103
|
+
the creation calls and the data calls are covered without touching call sites.
|
|
104
|
+
- `pl-middle-layer/src/js_render/index.ts` opens a render span per block, and one
|
|
105
|
+
per resumption of a deferred render. Driver calls carry no block identity of
|
|
106
|
+
their own, so the enclosing span is what attributes a join to a block.
|
|
107
|
+
|
|
108
|
+
`executeSingleLambda` is deliberately not instrumented: it evaluates `args()`
|
|
109
|
+
style lambdas with no computable context, so it reaches no driver.
|
|
110
|
+
|
|
111
|
+
## Known gaps
|
|
112
|
+
|
|
113
|
+
- The synchronous append cost is unmeasured on a real project; the sampler
|
|
114
|
+
interval and event granularity should be revisited with that number in hand.
|
|
115
|
+
- A table view can die in the renderer process rather than in the middle-layer
|
|
116
|
+
worker. That path needs `render-process-gone` in the desktop app and is not
|
|
117
|
+
covered here.
|
|
118
|
+
- Domain values are kept because they carry join identity. If any producer puts
|
|
119
|
+
a sample name in a domain value, it needs hashing too.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { CrashMarker, SessionEnvironment } from "./events.js";
|
|
2
|
+
import { FindingSeverity } from "./rules.js";
|
|
3
|
+
//#region src/analyze.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Turns a flight log into an attributed cause.
|
|
6
|
+
*
|
|
7
|
+
* Three independent lines of evidence are combined: which operation was still
|
|
8
|
+
* running when the process died (an unmatched begin record), where memory
|
|
9
|
+
* actually went (resident growth across each completed operation, and which
|
|
10
|
+
* region grew — JS heap, off-heap buffers, or native), and what the join tree
|
|
11
|
+
* looked like before any data was touched. Any one alone is suggestive;
|
|
12
|
+
* together they name a specific call in a specific block.
|
|
13
|
+
*/
|
|
14
|
+
export declare const THRESHOLDS: {
|
|
15
|
+
/** Fraction of the heap ceiling above which the heap counts as exhausted. */
|
|
16
|
+
readonly heapPressure: 0.85;
|
|
17
|
+
readonly nativeGrowthBytes: number;
|
|
18
|
+
readonly amplification: 10;
|
|
19
|
+
readonly unboundedRows: 1000000;
|
|
20
|
+
readonly returnedBytes: number;
|
|
21
|
+
readonly inlineEntries: 1000000;
|
|
22
|
+
readonly stallMs: 2000;
|
|
23
|
+
/** Backward tolerance when matching a marker by time, for parent/worker clock drift. */
|
|
24
|
+
readonly clockToleranceMs: 1000;
|
|
25
|
+
/** How many open sessions are considered as rivals for an unattributed marker. */
|
|
26
|
+
readonly maxRivalSessions: 8;
|
|
27
|
+
/** A machine-memory claim needs the process to actually be large. */
|
|
28
|
+
readonly machineRssShare: 0.25;
|
|
29
|
+
};
|
|
30
|
+
export type Finding = {
|
|
31
|
+
rule: string;
|
|
32
|
+
severity: FindingSeverity;
|
|
33
|
+
detail: string;
|
|
34
|
+
seq?: number;
|
|
35
|
+
path?: string;
|
|
36
|
+
join?: string;
|
|
37
|
+
source?: string;
|
|
38
|
+
block?: string;
|
|
39
|
+
};
|
|
40
|
+
export type MemoryAnalysis = {
|
|
41
|
+
samplerPresent: boolean;
|
|
42
|
+
sampleCount: number;
|
|
43
|
+
rssSeries: {
|
|
44
|
+
wall: number;
|
|
45
|
+
rss?: number;
|
|
46
|
+
freeMemory?: number;
|
|
47
|
+
}[];
|
|
48
|
+
peakRss: number;
|
|
49
|
+
rssAtDeath?: number;
|
|
50
|
+
rssGrowth: number;
|
|
51
|
+
heapUsedAtDeath?: number;
|
|
52
|
+
heapLimit?: number;
|
|
53
|
+
heapPressure?: number;
|
|
54
|
+
heapGrowth?: number;
|
|
55
|
+
externalGrowth?: number;
|
|
56
|
+
arrayBuffersGrowth?: number;
|
|
57
|
+
worstStallMs: number;
|
|
58
|
+
freeMemoryAtDeath?: number;
|
|
59
|
+
totalMemory?: number;
|
|
60
|
+
};
|
|
61
|
+
export type OperationSummary = {
|
|
62
|
+
op: string;
|
|
63
|
+
seq: number;
|
|
64
|
+
wall: number;
|
|
65
|
+
info: Record<string, unknown>;
|
|
66
|
+
end?: Record<string, unknown>;
|
|
67
|
+
ms?: number;
|
|
68
|
+
failed?: boolean;
|
|
69
|
+
rssDelta?: number;
|
|
70
|
+
heapDelta?: number;
|
|
71
|
+
};
|
|
72
|
+
export type RenderSummary = {
|
|
73
|
+
seq: number;
|
|
74
|
+
blockId?: string;
|
|
75
|
+
block?: string;
|
|
76
|
+
key?: string;
|
|
77
|
+
end?: boolean;
|
|
78
|
+
failed?: boolean;
|
|
79
|
+
ms?: number;
|
|
80
|
+
stats?: {
|
|
81
|
+
serOutBytes?: number;
|
|
82
|
+
serInBytes?: number;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
export type Verdict = {
|
|
87
|
+
outcome: string;
|
|
88
|
+
peakRss: number;
|
|
89
|
+
where: string;
|
|
90
|
+
memoryRegion?: string;
|
|
91
|
+
likelyCause?: string;
|
|
92
|
+
summary: string;
|
|
93
|
+
};
|
|
94
|
+
export type SessionAnalysis = {
|
|
95
|
+
file: string;
|
|
96
|
+
sessionId: string;
|
|
97
|
+
crashed: boolean;
|
|
98
|
+
truncatedTail: boolean;
|
|
99
|
+
endedReason?: string;
|
|
100
|
+
crashMarker?: CrashMarker;
|
|
101
|
+
env?: SessionEnvironment;
|
|
102
|
+
role?: string;
|
|
103
|
+
meta?: Record<string, unknown>;
|
|
104
|
+
recordCount: number;
|
|
105
|
+
/**
|
|
106
|
+
* How many times the log rotated. Each rotation re-emits the session header,
|
|
107
|
+
* so environment and metadata survive, but operations older than the retained
|
|
108
|
+
* segments do not — which is why the count is reported rather than implied.
|
|
109
|
+
*/
|
|
110
|
+
rotations: number;
|
|
111
|
+
memory: MemoryAnalysis;
|
|
112
|
+
/** Completed operations ranked by resident growth. */
|
|
113
|
+
attribution: OperationSummary[];
|
|
114
|
+
inFlight: OperationSummary[];
|
|
115
|
+
/** The innermost operation that started and never returned. */
|
|
116
|
+
inFlightAtDeath?: OperationSummary;
|
|
117
|
+
renders: RenderSummary[];
|
|
118
|
+
findings: Finding[];
|
|
119
|
+
verdict: Verdict;
|
|
120
|
+
timeline: Record<string, unknown>[];
|
|
121
|
+
};
|
|
122
|
+
/** Analyzes the newest crashed session in a directory, else the newest session. */
|
|
123
|
+
export declare function analyzeLatest(dir: string, options?: {
|
|
124
|
+
preferCrashed?: boolean;
|
|
125
|
+
}): SessionAnalysis | undefined;
|
|
126
|
+
/** Analyzes one flight log, merging the sibling sampler series when present. */
|
|
127
|
+
export declare function analyzeSession(file: string, dir?: string): SessionAnalysis;
|
|
128
|
+
/** Thousands separators, or `unknown` when the count was never observed. */
|
|
129
|
+
export declare function formatCount(value: number | undefined): string;
|
|
130
|
+
/** Binary byte units, or `unknown`. */
|
|
131
|
+
export declare function formatBytes(value: number | undefined): string;
|
|
132
|
+
//#endregion
|
|
133
|
+
//# sourceMappingURL=analyze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","names":[],"sources":["../src/analyze.ts"],"mappings":";;;;;;;;;;;;;qBAwBa;;WAEX;WACA;WACA;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;YAGU;EACV;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;;YAGU;EACV;EACA;EACA;IAAa;IAAc;IAAc;;EACzC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA,MAAM;EACN,MAAM;EACN;EACA;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IAAU;IAAsB;KAAsB;;;YAG5C;EACV;EACA;EACA;EACA;EACA;EACA;;YAGU;EACV;EACA;EACA;EACA;EACA;EACA,cAAc;EACd,MAAM;EACN;EACA,OAAO;EACP;;;;;;EAMA;EACA,QAAQ;;EAER,aAAa;EACb,UAAU;;EAEV,kBAAkB;EAClB,SAAS;EACT,UAAU;EACV,SAAS;EACT,UAAU;;;wBAII,cACd,aACA;EAAW;IACV;;wBASa,eAAe,cAAc,eAAmC;;wBA+DhE,YAAY;;wBAKZ,YAAY"}
|