@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.
Files changed (72) hide show
  1. package/README.md +119 -0
  2. package/dist/analyze.d.ts +133 -0
  3. package/dist/analyze.d.ts.map +1 -0
  4. package/dist/analyze.js +525 -0
  5. package/dist/analyze.js.map +1 -0
  6. package/dist/data_summary.d.ts +28 -0
  7. package/dist/data_summary.d.ts.map +1 -0
  8. package/dist/data_summary.js +73 -0
  9. package/dist/data_summary.js.map +1 -0
  10. package/dist/digest.d.ts +28 -0
  11. package/dist/digest.d.ts.map +1 -0
  12. package/dist/digest.js +55 -0
  13. package/dist/digest.js.map +1 -0
  14. package/dist/events.d.ts +89 -0
  15. package/dist/events.d.ts.map +1 -0
  16. package/dist/events.js +12 -0
  17. package/dist/events.js.map +1 -0
  18. package/dist/index.d.ts +13 -0
  19. package/dist/index.js +13 -0
  20. package/dist/instrument.d.ts +82 -0
  21. package/dist/instrument.d.ts.map +1 -0
  22. package/dist/instrument.js +313 -0
  23. package/dist/instrument.js.map +1 -0
  24. package/dist/recorder.d.ts +82 -0
  25. package/dist/recorder.d.ts.map +1 -0
  26. package/dist/recorder.js +293 -0
  27. package/dist/recorder.js.map +1 -0
  28. package/dist/redact.d.ts +53 -0
  29. package/dist/redact.d.ts.map +1 -0
  30. package/dist/redact.js +145 -0
  31. package/dist/redact.js.map +1 -0
  32. package/dist/report.d.ts +6 -0
  33. package/dist/report.d.ts.map +1 -0
  34. package/dist/report.js +377 -0
  35. package/dist/report.js.map +1 -0
  36. package/dist/rules.d.ts +73 -0
  37. package/dist/rules.d.ts.map +1 -0
  38. package/dist/rules.js +245 -0
  39. package/dist/rules.js.map +1 -0
  40. package/dist/sampler.d.ts +22 -0
  41. package/dist/sampler.d.ts.map +1 -0
  42. package/dist/sampler.js +33 -0
  43. package/dist/sampler.js.map +1 -0
  44. package/dist/sampler_thread.d.ts +1 -0
  45. package/dist/sampler_thread.js +41 -0
  46. package/dist/sampler_thread.js.map +1 -0
  47. package/dist/session.d.ts +40 -0
  48. package/dist/session.d.ts.map +1 -0
  49. package/dist/session.js +50 -0
  50. package/dist/session.js.map +1 -0
  51. package/dist/supervisor.d.ts +58 -0
  52. package/dist/supervisor.d.ts.map +1 -0
  53. package/dist/supervisor.js +108 -0
  54. package/dist/supervisor.js.map +1 -0
  55. package/package.json +43 -0
  56. package/src/analyze.test.ts +539 -0
  57. package/src/analyze.ts +795 -0
  58. package/src/data_summary.ts +110 -0
  59. package/src/digest.ts +49 -0
  60. package/src/events.ts +102 -0
  61. package/src/index.ts +104 -0
  62. package/src/instrument.ts +442 -0
  63. package/src/recorder.ts +397 -0
  64. package/src/redact.test.ts +155 -0
  65. package/src/redact.ts +213 -0
  66. package/src/report.ts +512 -0
  67. package/src/rules.test.ts +182 -0
  68. package/src/rules.ts +383 -0
  69. package/src/sampler.ts +40 -0
  70. package/src/sampler_thread.ts +45 -0
  71. package/src/session.ts +69 -0
  72. package/src/supervisor.ts +150 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Counts and sizes for a column payload, never the payload.
3
+ *
4
+ * This is the one place that has to know the shape of `DataInfo`, because the
5
+ * numbers that predict a join's cost — rows per partition and their byte sizes —
6
+ * live at type-specific positions inside it. Everything else about a definition
7
+ * is recorded structurally.
8
+ *
9
+ * Chunk statistics are optional: the producing workflow fills them in, so row
10
+ * counts are reported when present and left unknown otherwise rather than
11
+ * guessed.
12
+ */
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
+
27
+ export function summarizeData(data: unknown): DataSummary {
28
+ if (data === null || data === undefined) return { kind: "absent" };
29
+ if (Array.isArray(data)) {
30
+ // Inline values, built inside the model sandbox.
31
+ return { kind: "inline", entries: data.length, approxBytes: approxInlineBytes(data) };
32
+ }
33
+ if (typeof data !== "object") return { kind: typeof data };
34
+
35
+ const info = data as { type?: string; [key: string]: unknown };
36
+ switch (info.type) {
37
+ case "Json":
38
+ return {
39
+ kind: "Json",
40
+ keyLength: numberOr(info.keyLength),
41
+ entries: countKeys(info.data),
42
+ };
43
+ case "JsonPartitioned":
44
+ case "BinaryPartitioned":
45
+ return {
46
+ kind: info.type,
47
+ partitionKeyLength: numberOr(info.partitionKeyLength),
48
+ parts: countKeys(info.parts),
49
+ };
50
+ case "ParquetPartitioned":
51
+ return summarizeParquet(info);
52
+ default:
53
+ return { kind: info.type ?? opaqueKind(data) };
54
+ }
55
+ }
56
+
57
+ // Internals
58
+
59
+ function summarizeParquet(info: { [key: string]: unknown }): DataSummary {
60
+ const parts = Object.values((info.parts ?? {}) as Record<string, unknown>);
61
+ let rows = 0;
62
+ let bytes = 0;
63
+ let withStats = 0;
64
+ for (const part of parts) {
65
+ const stats = (
66
+ part as { stats?: { numberOfRows?: number; size?: { axes?: number[]; column?: number } } }
67
+ )?.stats;
68
+ if (!stats) continue;
69
+ withStats++;
70
+ if (typeof stats.numberOfRows === "number") rows += stats.numberOfRows;
71
+ if (stats.size) bytes += (stats.size.column ?? 0) + sum(stats.size.axes ?? []);
72
+ }
73
+ return {
74
+ kind: "ParquetPartitioned",
75
+ partitionKeyLength: numberOr(info.partitionKeyLength),
76
+ parts: parts.length,
77
+ partsWithStats: withStats,
78
+ rows: withStats > 0 ? rows : undefined,
79
+ bytes: withStats > 0 ? bytes : undefined,
80
+ };
81
+ }
82
+
83
+ // Sampled rather than measured: walking millions of entries to size them is
84
+ // itself a memory risk in the situation this code exists to diagnose.
85
+ function approxInlineBytes(values: unknown[]): number {
86
+ const sampleSize = Math.min(values.length, 64);
87
+ if (sampleSize === 0) return 0;
88
+ let bytes = 0;
89
+ for (let i = 0; i < sampleSize; i++) {
90
+ const value = values[Math.floor((i * values.length) / sampleSize)];
91
+ bytes += JSON.stringify(value ?? null)?.length ?? 0;
92
+ }
93
+ return Math.round((bytes / sampleSize) * values.length);
94
+ }
95
+
96
+ function countKeys(value: unknown): number | undefined {
97
+ return value && typeof value === "object" ? Object.keys(value).length : undefined;
98
+ }
99
+
100
+ function numberOr(value: unknown): number | undefined {
101
+ return typeof value === "number" ? value : undefined;
102
+ }
103
+
104
+ function opaqueKind(value: object): string {
105
+ return (value as { constructor?: { name?: string } }).constructor?.name ?? "opaque";
106
+ }
107
+
108
+ function sum(values: number[]): number {
109
+ return values.reduce((acc, value) => acc + value, 0);
110
+ }
package/src/digest.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { redact, type RedactionStats } from "./redact";
2
+
3
+ /**
4
+ * What a definition record carries.
5
+ *
6
+ * The definition is recorded structurally (see `redact`) rather than through a
7
+ * hand-written digest per definition type. Structural rules are not run here:
8
+ * they belong to the analyzer, so nothing is computed on the hot path and the
9
+ * rules can be revised against logs that already exist.
10
+ */
11
+
12
+ export const REDACTION = {
13
+ kept: [
14
+ "definition shape",
15
+ "column and axis names",
16
+ "value types",
17
+ "axis domains",
18
+ "filter operators",
19
+ "row, byte and partition counts",
20
+ ],
21
+ hashed: ["filter reference values", "annotation values", "column ids", "every other string"],
22
+ dropped: ["cell values", "inline column payloads", "partition keys"],
23
+ } as const;
24
+
25
+ export type DefKind = "PTableDef" | "PTableDefV2" | "PFrameDef";
26
+
27
+ export type DefDigest = {
28
+ kind: DefKind;
29
+ /** Redacted definition, same shape as the original. */
30
+ def: unknown;
31
+ redaction: RedactionStats & { bytes: number };
32
+ };
33
+
34
+ /** Records one definition: redacted, measured, and tagged with its API shape. */
35
+ export function digestDef(kind: DefKind, def: unknown): DefDigest {
36
+ const { value, stats } = redact(def);
37
+ const json = safeLength(value);
38
+ return { kind, def: value, redaction: { ...stats, bytes: json } };
39
+ }
40
+
41
+ // Internals
42
+
43
+ function safeLength(value: unknown): number {
44
+ try {
45
+ return JSON.stringify(value)?.length ?? 0;
46
+ } catch {
47
+ return 0;
48
+ }
49
+ }
package/src/events.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Record types written to a flight log.
3
+ *
4
+ * The log is append-only NDJSON, one record per line, and is read back by
5
+ * tooling that may be older or newer than the writer, so every field beyond
6
+ * {@link FlightRecordBase} is optional and unknown record types are skipped
7
+ * rather than rejected.
8
+ */
9
+
10
+ /** Memory reading taken by the thread that wrote the record. */
11
+ export type MemorySnapshot = {
12
+ /** Resident set size of the whole process. */
13
+ rss: number;
14
+ /** Heap in use by the writing thread's isolate. */
15
+ heapUsed: number;
16
+ heapTotal: number;
17
+ external: number;
18
+ arrayBuffers: number;
19
+ /** V8 heap ceiling for the writing thread's isolate. */
20
+ heapLimit: number;
21
+ };
22
+
23
+ export type FlightRecordBase = {
24
+ /** Monotonically increasing within one session; used to pair begin with end. */
25
+ seq: number;
26
+ /** Milliseconds since process start, for durations. */
27
+ t: number;
28
+ /** Wall clock, for correlating with the sampler series and crash markers. */
29
+ wall: number;
30
+ type: string;
31
+ };
32
+
33
+ export type FlightRecord = FlightRecordBase & {
34
+ mem?: MemorySnapshot;
35
+ /** Sequence number of the matching begin record, on end and error records. */
36
+ begin?: number;
37
+ [key: string]: unknown;
38
+ };
39
+
40
+ /** Session header, always the first record. */
41
+ export type SessionEnvironment = {
42
+ node: string;
43
+ platform: string;
44
+ cpus: number;
45
+ totalMemory: number;
46
+ heapLimit: number;
47
+ execArgv: string[];
48
+ maxOldSpaceSize?: number;
49
+ };
50
+
51
+ /** Written by the sampler thread to its own sibling file. */
52
+ export type SamplerRecord = FlightRecordBase & {
53
+ type: "mem-sampler";
54
+ rss: number;
55
+ peakRss: number;
56
+ freeMemory: number;
57
+ totalMemory: number;
58
+ };
59
+
60
+ /** Written by the parent when a supervised thread or process dies. */
61
+ export type CrashMarker = {
62
+ type: "external-crash";
63
+ wall: number;
64
+ /**
65
+ * Session the marker belongs to. Present only when the parent assigned the id
66
+ * to the worker and therefore knows it; never inferred, because a wrong id
67
+ * here would both misattribute the death and stop the right session from
68
+ * claiming it.
69
+ */
70
+ sessionId?: string;
71
+ /**
72
+ * Advisory only: the newest open flight log at the moment of death. A
73
+ * concurrent live session can make this wrong, so it is never matched against
74
+ * — it exists to help a human read a directory by hand.
75
+ */
76
+ guessedSessionId?: string;
77
+ /** Written by an older recorder that put a guess in `sessionId`. */
78
+ sessionIdSource?: "assigned" | "guessed";
79
+ reason: CrashReason;
80
+ errorCode?: string;
81
+ errorName?: string;
82
+ message?: string;
83
+ exitCode?: number;
84
+ signal?: string;
85
+ stderrTail?: string;
86
+ };
87
+
88
+ export type CrashReason =
89
+ | "js-heap-out-of-memory"
90
+ | "killed-by-os"
91
+ | "abort-or-fatal-allocation-failure"
92
+ | "worker-exit"
93
+ | "nonzero-exit"
94
+ | "unknown";
95
+
96
+ export const SESSION_RECORD = "session";
97
+ /** Earliest memory reading of a session, rewritten into every rotated segment. */
98
+ export const MEM_BASELINE_RECORD = "mem-baseline";
99
+ export const SESSION_END_RECORD = "session-end";
100
+ export const FLIGHT_FILE_PREFIX = "flight";
101
+ export const SAMPLER_FILE_PREFIX = "mem";
102
+ export const CRASH_FILE_PREFIX = "crash";
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
1
+ export {
2
+ openRecorder,
3
+ newSessionId,
4
+ startSelfSampler,
5
+ listSessions,
6
+ readSession,
7
+ sessionIdFromFile,
8
+ type Recorder,
9
+ type RecorderOptions,
10
+ type SessionFileInfo,
11
+ type ParsedSession,
12
+ } from "./recorder";
13
+
14
+ export { startMemorySampler, type MemorySampler, type MemorySamplerOptions } from "./sampler";
15
+
16
+ export {
17
+ openFlightSession,
18
+ FLIGHT_DIR_ENV,
19
+ FLIGHT_SESSION_ENV,
20
+ type FlightSession,
21
+ type FlightSessionOptions,
22
+ } from "./session";
23
+
24
+ export {
25
+ writeCrashMarker,
26
+ readCrashMarkers,
27
+ superviseWorker,
28
+ type CrashMarkerInput,
29
+ type SupervisedWorker,
30
+ type SuperviseOptions,
31
+ } from "./supervisor";
32
+
33
+ export {
34
+ wrapModelDriver,
35
+ wrapDataDriver,
36
+ recordModelRender,
37
+ recordModelRenderSync,
38
+ createHandleRegistry,
39
+ type HandleRegistry,
40
+ type HandleOrigin,
41
+ type RenderInfo,
42
+ } from "./instrument";
43
+
44
+ export { digestDef, REDACTION, type DefDigest, type DefKind } from "./digest";
45
+
46
+ export {
47
+ redact,
48
+ hashString,
49
+ isHashedString,
50
+ SCHEMA_KEYS,
51
+ SCHEMA_SUBTREE_KEYS,
52
+ SUMMARISED_KEYS,
53
+ COUNTED_KEYS,
54
+ type RedactionStats,
55
+ type RedactOptions,
56
+ type HashedString,
57
+ } from "./redact";
58
+
59
+ export { summarizeData, type DataSummary } from "./data_summary";
60
+
61
+ export {
62
+ structuralFindings,
63
+ joinShapes,
64
+ inputRowsMax,
65
+ axesUnder,
66
+ axisKey,
67
+ axisNameKey,
68
+ isJoinNode,
69
+ joinChildren,
70
+ type StructuralFinding,
71
+ type FindingSeverity,
72
+ type JoinShape,
73
+ type AxisDescriptor,
74
+ } from "./rules";
75
+
76
+ export {
77
+ analyzeSession,
78
+ analyzeLatest,
79
+ formatBytes,
80
+ formatCount,
81
+ THRESHOLDS,
82
+ type SessionAnalysis,
83
+ type Finding,
84
+ type MemoryAnalysis,
85
+ type OperationSummary,
86
+ type RenderSummary,
87
+ type Verdict,
88
+ } from "./analyze";
89
+
90
+ export { renderReport } from "./report";
91
+
92
+ export {
93
+ FLIGHT_FILE_PREFIX,
94
+ SAMPLER_FILE_PREFIX,
95
+ CRASH_FILE_PREFIX,
96
+ SESSION_RECORD,
97
+ SESSION_END_RECORD,
98
+ type FlightRecord,
99
+ type MemorySnapshot,
100
+ type SamplerRecord,
101
+ type CrashMarker,
102
+ type CrashReason,
103
+ type SessionEnvironment,
104
+ } from "./events";