@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,442 @@
|
|
|
1
|
+
import type { PColumn, PTableDef, PTableDefV2 } from "@milaboratories/pl-model-common";
|
|
2
|
+
import { digestDef, type DefDigest, type DefKind } from "./digest";
|
|
3
|
+
import { redact } from "./redact";
|
|
4
|
+
import type { Recorder } from "./recorder";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wrappers for the seams the model layer passes through.
|
|
8
|
+
*
|
|
9
|
+
* Every operation writes a begin record and an end record. That pairing is what
|
|
10
|
+
* makes a crash legible: when the process dies mid-operation the end record is
|
|
11
|
+
* missing, so the log names the exact call that was running when memory ran out
|
|
12
|
+
* — the question a post-crash report has to answer.
|
|
13
|
+
*
|
|
14
|
+
* The wrappers are structural rather than tied to one driver interface, because
|
|
15
|
+
* the same three creation methods appear twice with different return types: the
|
|
16
|
+
* model-facing driver hands back a bare handle, the internal one hands back a
|
|
17
|
+
* pool entry.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type HandleOrigin = {
|
|
21
|
+
/** Sequence number of the record holding the definition. */
|
|
22
|
+
seq: number;
|
|
23
|
+
op: string;
|
|
24
|
+
observed?: { rows?: number; columns?: number };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type HandleRegistry = {
|
|
28
|
+
put(handle: string, origin: HandleOrigin): void;
|
|
29
|
+
get(handle: string): HandleOrigin | undefined;
|
|
30
|
+
observe(handle: string, observed: { rows?: number; columns?: number }): void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ModelDriverLike<H> = {
|
|
34
|
+
createPFrame(def: never): H;
|
|
35
|
+
createPTable(def: never): H;
|
|
36
|
+
createPTableV2(def: never): H;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type RenderInfo = {
|
|
40
|
+
blockId?: string;
|
|
41
|
+
block?: string;
|
|
42
|
+
blockVersion?: string;
|
|
43
|
+
key?: string;
|
|
44
|
+
argsHash?: string;
|
|
45
|
+
/** Which lambda of the block's model is being rendered. */
|
|
46
|
+
lambda?: string;
|
|
47
|
+
/** Nth resumption of a deferred render, counted from one. */
|
|
48
|
+
recalculation?: number;
|
|
49
|
+
/** Read after the render, so sandbox counters cover the whole call. */
|
|
50
|
+
getStats?: () => unknown;
|
|
51
|
+
/** Any further context the call site wants on the record. */
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Maps driver handles back to the join that produced them. */
|
|
56
|
+
export function createHandleRegistry(limit = 512): HandleRegistry {
|
|
57
|
+
const map = new Map<string, HandleOrigin>();
|
|
58
|
+
return {
|
|
59
|
+
put(handle, origin) {
|
|
60
|
+
if (map.size >= limit) {
|
|
61
|
+
const oldest = map.keys().next();
|
|
62
|
+
if (!oldest.done) map.delete(oldest.value);
|
|
63
|
+
}
|
|
64
|
+
map.set(handle, origin);
|
|
65
|
+
},
|
|
66
|
+
get(handle) {
|
|
67
|
+
return map.get(handle);
|
|
68
|
+
},
|
|
69
|
+
observe(handle, observed) {
|
|
70
|
+
const origin = map.get(handle);
|
|
71
|
+
if (origin) origin.observed = observed;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Wraps the driver that block models call to build frames and tables.
|
|
78
|
+
*
|
|
79
|
+
* Records the redacted join tree and its structural findings, then remembers
|
|
80
|
+
* which handle came from which join so later data calls can be attributed back
|
|
81
|
+
* to the definition that caused them.
|
|
82
|
+
*/
|
|
83
|
+
export function wrapModelDriver<D extends ModelDriverLike<unknown>>(
|
|
84
|
+
driver: D,
|
|
85
|
+
recorder: Recorder,
|
|
86
|
+
registry: HandleRegistry,
|
|
87
|
+
handleOf: (result: unknown) => string = defaultHandleOf,
|
|
88
|
+
): D {
|
|
89
|
+
const wrapped = {
|
|
90
|
+
createPFrame(def: readonly PColumn<unknown>[]) {
|
|
91
|
+
return record(recorder, registry, handleOf, "createPFrame", "PFrameDef", def, () =>
|
|
92
|
+
(driver.createPFrame as (d: unknown) => unknown)(def),
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
createPTable(def: PTableDef<PColumn<unknown>>) {
|
|
96
|
+
return record(recorder, registry, handleOf, "createPTable", "PTableDef", def, () =>
|
|
97
|
+
(driver.createPTable as (d: unknown) => unknown)(def),
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
createPTableV2(def: PTableDefV2<PColumn<unknown>>) {
|
|
101
|
+
return record(recorder, registry, handleOf, "createPTableV2", "PTableDefV2", def, () =>
|
|
102
|
+
(driver.createPTableV2 as (d: unknown) => unknown)(def),
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
// The three creation methods are replaced and everything else is inherited,
|
|
107
|
+
// so the wrapper satisfies whichever driver interface the caller holds.
|
|
108
|
+
return Object.assign(Object.create(driver as object) as D, wrapped);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Wraps the asynchronous data-access driver.
|
|
113
|
+
*
|
|
114
|
+
* Adds the observed table shape, the size of what crossed back into JavaScript,
|
|
115
|
+
* and the amplification between input and output rows — the empirical
|
|
116
|
+
* counterpart to the structural findings taken from the join tree.
|
|
117
|
+
*/
|
|
118
|
+
export function wrapDataDriver<D extends object>(
|
|
119
|
+
driver: D,
|
|
120
|
+
recorder: Recorder,
|
|
121
|
+
registry: HandleRegistry,
|
|
122
|
+
): D {
|
|
123
|
+
const source = driver as unknown as {
|
|
124
|
+
getShape(handle: string, ...rest: unknown[]): Promise<{ rows: number; columns: number }>;
|
|
125
|
+
getData(
|
|
126
|
+
handle: string,
|
|
127
|
+
columnIndices: number[],
|
|
128
|
+
range?: { offset: number; length: number },
|
|
129
|
+
...rest: unknown[]
|
|
130
|
+
): Promise<unknown[]>;
|
|
131
|
+
calculateTableData(handle: string, request: unknown, ...rest: unknown[]): Promise<unknown[]>;
|
|
132
|
+
getUniqueValues(
|
|
133
|
+
handle: string,
|
|
134
|
+
request: unknown,
|
|
135
|
+
...rest: unknown[]
|
|
136
|
+
): Promise<{ values?: { data?: unknown }; overflow?: boolean }>;
|
|
137
|
+
findColumns(
|
|
138
|
+
handle: string,
|
|
139
|
+
request: unknown,
|
|
140
|
+
...rest: unknown[]
|
|
141
|
+
): Promise<{ hits?: unknown[] }>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const wrapped = {
|
|
145
|
+
async getShape(handle: string, ...rest: unknown[]) {
|
|
146
|
+
const origin = registry.get(handle);
|
|
147
|
+
return await span(
|
|
148
|
+
recorder,
|
|
149
|
+
"getShape",
|
|
150
|
+
{ handle: shortHandle(handle), joinSeq: origin?.seq },
|
|
151
|
+
async () => {
|
|
152
|
+
const shape = await source.getShape(handle, ...rest);
|
|
153
|
+
registry.observe(handle, { rows: shape?.rows, columns: shape?.columns });
|
|
154
|
+
// How much this join amplified is judged in the analyzer, which can
|
|
155
|
+
// read the definition this handle came from without repeating the
|
|
156
|
+
// work here, on the hot path.
|
|
157
|
+
return { result: shape, detail: { rows: shape?.rows, columns: shape?.columns } };
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async getData(
|
|
163
|
+
handle: string,
|
|
164
|
+
columnIndices: number[],
|
|
165
|
+
range?: { offset: number; length: number },
|
|
166
|
+
...rest: unknown[]
|
|
167
|
+
) {
|
|
168
|
+
const origin = registry.get(handle);
|
|
169
|
+
return await span(
|
|
170
|
+
recorder,
|
|
171
|
+
"getData",
|
|
172
|
+
{
|
|
173
|
+
handle: shortHandle(handle),
|
|
174
|
+
joinSeq: origin?.seq,
|
|
175
|
+
columnCount: columnIndices?.length,
|
|
176
|
+
range: range ? { offset: range.offset, length: range.length } : null,
|
|
177
|
+
// A fetch with no range pulls the whole table into the JS heap, which
|
|
178
|
+
// on a large table is an out-of-memory condition by itself.
|
|
179
|
+
unbounded: !range,
|
|
180
|
+
tableRows: origin?.observed?.rows,
|
|
181
|
+
},
|
|
182
|
+
async () => {
|
|
183
|
+
const data = await source.getData(handle, columnIndices, range, ...rest);
|
|
184
|
+
return { result: data, detail: { returnedBytes: vectorsBytes(data) } };
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async calculateTableData(handle: string, request: unknown, ...rest: unknown[]) {
|
|
190
|
+
return await span(
|
|
191
|
+
recorder,
|
|
192
|
+
"calculateTableData",
|
|
193
|
+
{ handle: shortHandle(handle), def: digestDef("PTableDef", request) },
|
|
194
|
+
async () => {
|
|
195
|
+
const data = await source.calculateTableData(handle, request, ...rest);
|
|
196
|
+
const vectors = (data ?? []).map((column) => (column as { data?: unknown })?.data);
|
|
197
|
+
return {
|
|
198
|
+
result: data,
|
|
199
|
+
detail: { columns: data?.length, returnedBytes: vectorsBytes(vectors) },
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Both of these reach the engine as well, and a filter that matches most of
|
|
206
|
+
// a large axis is a plausible place to run out of memory. The request is
|
|
207
|
+
// recorded redacted: axis identity survives, filter values become hashes.
|
|
208
|
+
async getUniqueValues(handle: string, request: unknown, ...rest: unknown[]) {
|
|
209
|
+
return await span(
|
|
210
|
+
recorder,
|
|
211
|
+
"getUniqueValues",
|
|
212
|
+
{ handle: shortHandle(handle), request: redact(request).value },
|
|
213
|
+
async () => {
|
|
214
|
+
const response = await source.getUniqueValues(handle, request, ...rest);
|
|
215
|
+
return {
|
|
216
|
+
result: response,
|
|
217
|
+
detail: {
|
|
218
|
+
uniqueValues: vectorLength(response?.values),
|
|
219
|
+
overflow: response?.overflow,
|
|
220
|
+
returnedBytes: vectorsBytes([response?.values]),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async findColumns(handle: string, request: unknown, ...rest: unknown[]) {
|
|
228
|
+
return await span(
|
|
229
|
+
recorder,
|
|
230
|
+
"findColumns",
|
|
231
|
+
{ handle: shortHandle(handle), request: redact(request).value },
|
|
232
|
+
async () => {
|
|
233
|
+
const response = await source.findColumns(handle, request, ...rest);
|
|
234
|
+
return { result: response, detail: { hits: response?.hits?.length } };
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return Object.assign(Object.create(driver) as D, wrapped);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Records one block model render.
|
|
245
|
+
*
|
|
246
|
+
* `getStats` exposes the middle layer's own sandbox accounting, whose
|
|
247
|
+
* serialisation byte counts show how much data the model moved across the
|
|
248
|
+
* QuickJS boundary — the model-layer memory cost that no driver call reports.
|
|
249
|
+
*/
|
|
250
|
+
export async function recordModelRender<T>(
|
|
251
|
+
recorder: Recorder | undefined,
|
|
252
|
+
info: RenderInfo,
|
|
253
|
+
fn: () => Promise<T>,
|
|
254
|
+
): Promise<T> {
|
|
255
|
+
if (!recorder) return await fn();
|
|
256
|
+
const { getStats, ...plain } = info;
|
|
257
|
+
const begin = recorder.event("render-begin", { ...plain, mem: recorder.memorySnapshot() });
|
|
258
|
+
const startedAt = performance.now();
|
|
259
|
+
try {
|
|
260
|
+
const result = await fn();
|
|
261
|
+
recorder.event("render-end", {
|
|
262
|
+
begin,
|
|
263
|
+
...plain,
|
|
264
|
+
ms: round(performance.now() - startedAt),
|
|
265
|
+
stats: getStats?.(),
|
|
266
|
+
mem: recorder.memorySnapshot(),
|
|
267
|
+
});
|
|
268
|
+
return result;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
recorder.event("render-error", {
|
|
271
|
+
begin,
|
|
272
|
+
...plain,
|
|
273
|
+
ms: round(performance.now() - startedAt),
|
|
274
|
+
error: describeError(error),
|
|
275
|
+
mem: recorder.memorySnapshot(),
|
|
276
|
+
});
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Synchronous variant, for a render that is not driven by a promise. */
|
|
282
|
+
export function recordModelRenderSync<T>(
|
|
283
|
+
recorder: Recorder | undefined,
|
|
284
|
+
info: RenderInfo,
|
|
285
|
+
fn: () => T,
|
|
286
|
+
): T {
|
|
287
|
+
if (!recorder) return fn();
|
|
288
|
+
const { getStats, ...plain } = info;
|
|
289
|
+
const begin = recorder.event("render-begin", { ...plain, mem: recorder.memorySnapshot() });
|
|
290
|
+
const startedAt = performance.now();
|
|
291
|
+
try {
|
|
292
|
+
const result = fn();
|
|
293
|
+
recorder.event("render-end", {
|
|
294
|
+
begin,
|
|
295
|
+
...plain,
|
|
296
|
+
ms: round(performance.now() - startedAt),
|
|
297
|
+
stats: getStats?.(),
|
|
298
|
+
mem: recorder.memorySnapshot(),
|
|
299
|
+
});
|
|
300
|
+
return result;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
recorder.event("render-error", {
|
|
303
|
+
begin,
|
|
304
|
+
...plain,
|
|
305
|
+
ms: round(performance.now() - startedAt),
|
|
306
|
+
error: describeError(error),
|
|
307
|
+
mem: recorder.memorySnapshot(),
|
|
308
|
+
});
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Internals
|
|
314
|
+
|
|
315
|
+
function record<R>(
|
|
316
|
+
recorder: Recorder,
|
|
317
|
+
registry: HandleRegistry,
|
|
318
|
+
handleOf: (result: unknown) => string,
|
|
319
|
+
op: string,
|
|
320
|
+
kind: DefKind,
|
|
321
|
+
def: unknown,
|
|
322
|
+
call: () => R,
|
|
323
|
+
): R {
|
|
324
|
+
let digest: DefDigest | { digestFailed: string };
|
|
325
|
+
try {
|
|
326
|
+
digest = digestDef(kind, def);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
// Diagnostics must never be the reason a join fails to build.
|
|
329
|
+
digest = { digestFailed: describeError(error) };
|
|
330
|
+
}
|
|
331
|
+
// A creation call is synchronous but not free: it hands the definition to the
|
|
332
|
+
// native engine, which can allocate. It gets a begin/end pair like any other
|
|
333
|
+
// operation, so a death inside it is attributed to it and not to the render
|
|
334
|
+
// around it.
|
|
335
|
+
const seq = recorder.event(`${op}-begin`, { def: digest, mem: recorder.memorySnapshot() });
|
|
336
|
+
const startedAt = performance.now();
|
|
337
|
+
|
|
338
|
+
let result: R;
|
|
339
|
+
try {
|
|
340
|
+
result = call();
|
|
341
|
+
} catch (error) {
|
|
342
|
+
recorder.event(`${op}-error`, {
|
|
343
|
+
begin: seq,
|
|
344
|
+
ms: round(performance.now() - startedAt),
|
|
345
|
+
error: describeError(error),
|
|
346
|
+
mem: recorder.memorySnapshot(),
|
|
347
|
+
});
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const handle = handleOf(result);
|
|
352
|
+
registry.put(handle, { seq, op });
|
|
353
|
+
recorder.event(`${op}-end`, {
|
|
354
|
+
begin: seq,
|
|
355
|
+
ms: round(performance.now() - startedAt),
|
|
356
|
+
handle: shortHandle(handle),
|
|
357
|
+
mem: recorder.memorySnapshot(),
|
|
358
|
+
});
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function span<T>(
|
|
363
|
+
recorder: Recorder,
|
|
364
|
+
op: string,
|
|
365
|
+
info: Record<string, unknown>,
|
|
366
|
+
fn: () => Promise<{ result: T; detail: Record<string, unknown> }>,
|
|
367
|
+
): Promise<T> {
|
|
368
|
+
const begin = recorder.event(`${op}-begin`, { ...info, mem: recorder.memorySnapshot() });
|
|
369
|
+
const startedAt = performance.now();
|
|
370
|
+
try {
|
|
371
|
+
const { result, detail } = await fn();
|
|
372
|
+
recorder.event(`${op}-end`, {
|
|
373
|
+
begin,
|
|
374
|
+
ms: round(performance.now() - startedAt),
|
|
375
|
+
...detail,
|
|
376
|
+
mem: recorder.memorySnapshot(),
|
|
377
|
+
});
|
|
378
|
+
return result;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
recorder.event(`${op}-error`, {
|
|
381
|
+
begin,
|
|
382
|
+
ms: round(performance.now() - startedAt),
|
|
383
|
+
error: describeError(error),
|
|
384
|
+
mem: recorder.memorySnapshot(),
|
|
385
|
+
});
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Accepts both a bare handle string and a pool entry wrapping one. */
|
|
391
|
+
function defaultHandleOf(result: unknown): string {
|
|
392
|
+
if (typeof result === "string") return result;
|
|
393
|
+
const key = (result as { key?: unknown } | null)?.key;
|
|
394
|
+
return typeof key === "string" ? key : String(result);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function vectorsBytes(vectors: unknown[]): number {
|
|
398
|
+
let bytes = 0;
|
|
399
|
+
for (const vector of vectors ?? []) {
|
|
400
|
+
const data = (vector as { data?: unknown; isNA?: { byteLength?: number } } | null)?.data;
|
|
401
|
+
if (!data) continue;
|
|
402
|
+
const byteLength = (data as { byteLength?: number }).byteLength;
|
|
403
|
+
if (typeof byteLength === "number") {
|
|
404
|
+
bytes += byteLength;
|
|
405
|
+
} else if (Array.isArray(data)) {
|
|
406
|
+
// String and Bytes columns arrive as plain arrays, and those are the ones
|
|
407
|
+
// that actually threaten the JS heap, so they are sampled not skipped.
|
|
408
|
+
bytes += sampledArrayBytes(data);
|
|
409
|
+
}
|
|
410
|
+
const isNA = (vector as { isNA?: { byteLength?: number } }).isNA;
|
|
411
|
+
if (typeof isNA?.byteLength === "number") bytes += isNA.byteLength;
|
|
412
|
+
}
|
|
413
|
+
return bytes;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function vectorLength(vector: unknown): number | undefined {
|
|
417
|
+
const data = (vector as { data?: { length?: number } } | undefined)?.data;
|
|
418
|
+
return typeof data?.length === "number" ? data.length : undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function sampledArrayBytes(data: unknown[]): number {
|
|
422
|
+
const sampleSize = Math.min(data.length, 32);
|
|
423
|
+
if (sampleSize === 0) return 0;
|
|
424
|
+
let sampled = 0;
|
|
425
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
426
|
+
const value = data[Math.floor((i * data.length) / sampleSize)];
|
|
427
|
+
sampled += typeof value === "string" ? value.length * 2 + 24 : 8;
|
|
428
|
+
}
|
|
429
|
+
return Math.round((sampled / sampleSize) * data.length);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function shortHandle(handle: string): string {
|
|
433
|
+
return typeof handle === "string" ? handle.slice(0, 24) : String(handle);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function describeError(error: unknown): string {
|
|
437
|
+
return String((error as { message?: unknown } | null)?.message ?? error);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function round(value: number): number {
|
|
441
|
+
return Math.round(value * 100) / 100;
|
|
442
|
+
}
|