qvdjs 2.2.0 → 2.2.1

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/dist/index.js CHANGED
@@ -2253,9 +2253,10 @@ function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, col
2253
2253
  }
2254
2254
  return low;
2255
2255
  }
2256
- function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = null, readBytes = null, wholeSymbols = false) {
2256
+ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = null, readBytes = null, wholeSymbols = false, retainedBytes = 0) {
2257
2257
  const answer = checkMemory({
2258
2258
  wholeSymbols,
2259
+ retainedBytes,
2259
2260
  symbolTableSize,
2260
2261
  maxRows,
2261
2262
  totalRows,
@@ -2288,7 +2289,8 @@ function checkMemory({
2288
2289
  bytesHeld = null,
2289
2290
  readBytes = null,
2290
2291
  measured = null,
2291
- wholeSymbols = false
2292
+ wholeSymbols = false,
2293
+ retainedBytes = 0
2292
2294
  }) {
2293
2295
  if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
2294
2296
  throw new QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", {
@@ -2312,7 +2314,12 @@ function checkMemory({
2312
2314
  rowsLive,
2313
2315
  wholeSymbols
2314
2316
  );
2315
- const { held, forRows: heldForRows, forChunk: heldForChunk } = bytesHeld ?? noBytesHeld;
2317
+ const {
2318
+ held,
2319
+ afterRelease: heldAfterRelease = null,
2320
+ forRows: heldForRows,
2321
+ forChunk: heldForChunk
2322
+ } = bytesHeld ?? noBytesHeld;
2316
2323
  const externalMemory = estimateExternalMemory(liveRows, columnCount) + held;
2317
2324
  const bounded = budget.candidates.map((candidate) => {
2318
2325
  const heapOnly = candidate.source === "V8 heap limit";
@@ -2348,6 +2355,25 @@ function checkMemory({
2348
2355
  const availableMemory = binding ? binding.bytes : budget.bytes;
2349
2356
  const estimatedMemory = binding ? binding.needs : heapMemory;
2350
2357
  const maxAllowedMemory = binding ? binding.allowed : budget.bytes * safetyFactor;
2358
+ const retainedIsTheReason = (() => {
2359
+ if (binding === null || retainedBytes <= 0) {
2360
+ return false;
2361
+ }
2362
+ const heapAfter = estimateMemoryUsage(
2363
+ Math.max(0, symbolTableSize - retainedBytes),
2364
+ maxRows,
2365
+ totalRows,
2366
+ columnCount,
2367
+ materialisesRows,
2368
+ rowsLive,
2369
+ wholeSymbols
2370
+ );
2371
+ const externalAfter = estimateExternalMemory(liveRows, columnCount) + (heldAfterRelease ?? held);
2372
+ return budget.candidates.every((candidate) => {
2373
+ const heapOnly = candidate.source === "V8 heap limit";
2374
+ return (heapOnly ? heapAfter : heapAfter + externalAfter) <= candidate.bytes * safetyFactor;
2375
+ });
2376
+ })();
2351
2377
  if (binding) {
2352
2378
  const includeExternal = !binding.heapOnly;
2353
2379
  const rowsHeldBudget = /* @__PURE__ */ __name((rows) => maxAllowedMemory - (includeExternal ? heldForRows(rows) : 0), "rowsHeldBudget");
@@ -2394,13 +2420,17 @@ function checkMemory({
2394
2420
  const knob = chunked ? "chunkSize" : "limit";
2395
2421
  const recommendedValue = chunked ? recommendedChunk : recommendedMaxRows;
2396
2422
  const nothingFits = recommendedValue === 0;
2423
+ const held2 = retainedIsTheReason;
2424
+ const fixedCost = held2 ? "the columns this file is holding exceed" : "the symbol table alone exceeds";
2425
+ const release = held2 ? `Close the file and open it again to release them, or raise ` : `Raise `;
2426
+ const releaseFirst = held2 ? `Close the file and open it again to release the columns it is holding, which is the direct remedy. ` : "";
2397
2427
  let advice;
2398
2428
  if (nothingFits) {
2399
- advice = `No row count fits this budget - the symbol table alone exceeds it, so ${knob} cannot help. ` + (containerBound ? `Raise the container's memory limit.` : `Raise the heap with --max-old-space-size, or raise memorySafetyFactor.`);
2429
+ advice = `No row count fits this budget - ${fixedCost} it, so ${knob} cannot help. ` + (containerBound ? `${release}the container's memory limit.` : `${release}the heap with --max-old-space-size, or raise memorySafetyFactor.`);
2400
2430
  } else if (containerBound) {
2401
- advice = `The binding limit is the container's, so raising --max-old-space-size would let V8 grow past it and be killed by the OOM killer instead. Set it below the container limit, raise the limit, or hold fewer rows with ${knob} (recommended: ${formatCount(recommendedValue)} rows or less).`;
2431
+ advice = releaseFirst + `The binding limit is the container's, so raising --max-old-space-size would let V8 grow past it and be killed by the OOM killer instead. Set it below the container limit, raise the limit, or hold fewer rows with ${knob} (recommended: ${formatCount(recommendedValue)} rows or less).`;
2402
2432
  } else {
2403
- advice = `Try holding fewer rows using the ${knob} parameter (recommended: ${formatCount(recommendedValue)} rows or less), or raise the heap with --max-old-space-size.`;
2433
+ advice = releaseFirst + `Try holding fewer rows using the ${knob} parameter (recommended: ${formatCount(recommendedValue)} rows or less), or raise the heap with --max-old-space-size.`;
2404
2434
  }
2405
2435
  const suggestions = [];
2406
2436
  if (!nothingFits) {
@@ -2424,10 +2454,16 @@ function checkMemory({
2424
2454
  exact: symbolTableSize === 0,
2425
2455
  suggestions,
2426
2456
  refusal: {
2427
- message: `Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}, which bounds ${limitingScope}; considered: ${budgetBreakdown}; observed but not used: ${observedBreakdown}). ` + advice,
2457
+ message: `Insufficient memory to load file safely. ${retainedIsTheReason ? "Columns held" : "Symbol table"}: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}, which bounds ${limitingScope}; considered: ${budgetBreakdown}; observed but not used: ${observedBreakdown}). ` + advice,
2428
2458
  context: {
2429
2459
  symbolTableSize,
2430
2460
  symbolTableSizeMB: sizeMB,
2461
+ // Whether that figure is the file's symbol table or what an open file is still holding. A
2462
+ // paging read is charged for every column any of its pages decoded and kept, so a caller
2463
+ // branching on the refusal needs to know which of the two it is looking at - the remedies
2464
+ // differ, and for this one releasing is a remedy where raising the limit is only a workaround.
2465
+ holdsDecodedColumns: retainedIsTheReason,
2466
+ retainedSymbolBytes: retainedBytes,
2431
2467
  estimatedMemoryMB: estimatedMB,
2432
2468
  availableMemoryMB: availableMB,
2433
2469
  heapLimitMB,
@@ -2595,19 +2631,39 @@ function validateHeaderStructure(headerObj, filePath, stage) {
2595
2631
  });
2596
2632
  return fieldList;
2597
2633
  }
2598
- function validateSymbolTableSizeEarly(symbolTableLength, filePath) {
2634
+ function validateSymbolTableSizeEarly(symbolTableLength, filePath, retainedBytes = 0) {
2599
2635
  const heapLimit = getHeapLimit();
2600
2636
  const MAX_SYMBOL_TABLE_SIZE = heapLimit * 0.125;
2601
2637
  if (symbolTableLength > MAX_SYMBOL_TABLE_SIZE) {
2602
2638
  const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
2603
2639
  const maxMB = Math.round(MAX_SYMBOL_TABLE_SIZE / 1024 / 1024);
2604
2640
  const heapMB = Math.round(heapLimit / 1024 / 1024);
2641
+ if (retainedBytes > 0 && symbolTableLength - retainedBytes <= MAX_SYMBOL_TABLE_SIZE) {
2642
+ throw new QvdValidationError(
2643
+ `Columns held too large (${sizeMB}MB exceeds ${maxMB}MB limit). This open file is holding the columns its pages have decoded, and they have grown past the ceiling rather than the file's own symbol table being large. Limit scales with heap size (current: ${heapMB}MB, limit: 12.5% = ${maxMB}MB). Consider: (1) closing the file and opening it again, which releases what the pages decoded, (2) paging over fewer columns with fields, or (3) increasing heap size with --max-old-space-size.`,
2644
+ {
2645
+ file: filePath,
2646
+ symbolTableSize: symbolTableLength,
2647
+ symbolTableSizeMB: sizeMB,
2648
+ holdsDecodedColumns: true,
2649
+ retainedSymbolBytes: retainedBytes,
2650
+ maxAllowed: MAX_SYMBOL_TABLE_SIZE,
2651
+ maxAllowedMB: maxMB,
2652
+ heapLimitMB: heapMB,
2653
+ reason: "memory"
2654
+ }
2655
+ );
2656
+ }
2605
2657
  throw new QvdValidationError(
2606
2658
  `Symbol table too large (${sizeMB}MB exceeds ${maxMB}MB limit for lazy loading). This QVD file contains extremely high-cardinality fields. Limit scales with heap size (current: ${heapMB}MB, limit: 12.5% = ${maxMB}MB). Consider: (1) loading the full file without a row window - maxRows, limit or offset - since the symbol table is read in full either way, (2) increasing heap size with --max-old-space-size, or (3) aggregating high-cardinality fields.`,
2607
2659
  {
2608
2660
  file: filePath,
2609
2661
  symbolTableSize: symbolTableLength,
2610
2662
  symbolTableSizeMB: sizeMB,
2663
+ // Present and false, not absent. A caller told it can branch on this has to find it on both
2664
+ // refusals, or the branch reads `undefined` for the commoner of the two.
2665
+ holdsDecodedColumns: false,
2666
+ retainedSymbolBytes: retainedBytes,
2611
2667
  maxAllowed: MAX_SYMBOL_TABLE_SIZE,
2612
2668
  maxAllowedMB: maxMB,
2613
2669
  heapLimitMB: heapMB,
@@ -4013,6 +4069,10 @@ var init_QvdFileReader = __esm({
4013
4069
  const columnCount = selected.length;
4014
4070
  const symbolBytes = symbolBytesOf(this._fieldsHeldAfter(selected, headerFields), symbolTableLength);
4015
4071
  const readSymbolBytes = symbolBytesOf(this._fieldsReadBy(selected), symbolTableLength);
4072
+ const retainedBytes = symbolBytesOf(
4073
+ this._fieldsHeldAfter(selected, headerFields).slice(selected.length),
4074
+ symbolTableLength
4075
+ );
4016
4076
  const resolved = headerNumbersUsable ? resolveWindow(window, totalRows) : { offset: 0, limit: 0 };
4017
4077
  const windowRows = resolved.limit;
4018
4078
  if (headerNumbersUsable && this._headerMatchesFile) {
@@ -4030,7 +4090,8 @@ var init_QvdFileReader = __esm({
4030
4090
  windowRows,
4031
4091
  recordSize,
4032
4092
  liveRows,
4033
- this._analysisAhead(window, resolved, totalRows, symbolTableLength)
4093
+ this._analysisAhead(window, resolved, totalRows, symbolTableLength),
4094
+ symbolBytes - retainedBytes
4034
4095
  ),
4035
4096
  // What it reads, which is not what it holds - the records go through one buffer and are not
4036
4097
  // kept. Carried so that a refusal's `check` says everything the pre-flight would have said.
@@ -4042,7 +4103,8 @@ var init_QvdFileReader = __esm({
4042
4103
  // A paging read keeps whole columns, so the estimate must not discount its symbols as a window's
4043
4104
  // sample of them - see `estimateMemoryUsage`. Under-charging is the direction that ends in a
4044
4105
  // heap-limit abort rather than an error.
4045
- this._symbolCache !== null
4106
+ this._symbolCache !== null,
4107
+ retainedBytes
4046
4108
  );
4047
4109
  }
4048
4110
  if (window.offset === 0 && window.limit === null) {
@@ -4050,7 +4112,7 @@ var init_QvdFileReader = __esm({
4050
4112
  return;
4051
4113
  }
4052
4114
  const rowsToLoad = windowRows;
4053
- validateSymbolTableSizeEarly(symbolBytes, this._path);
4115
+ validateSymbolTableSizeEarly(symbolBytes, this._path, retainedBytes);
4054
4116
  validateRecordSize(recordSize, this._path, "readData");
4055
4117
  validateRecordCount(totalRows, this._path, "readData");
4056
4118
  const fileBytesRequired = indexTableOffset + (resolved.offset + rowsToLoad) * recordSize;
@@ -4162,9 +4224,14 @@ var init_QvdFileReader = __esm({
4162
4224
  * would hold.
4163
4225
  * @private
4164
4226
  */
4165
- _bytesHeld(symbolBytes, windowRows, recordSize, liveRows, analysisAhead) {
4227
+ _bytesHeld(symbolBytes, windowRows, recordSize, liveRows, analysisAhead, freshSymbolBytes = null) {
4166
4228
  return {
4167
4229
  held: this._bytesHeldBy(symbolBytes, this._recordsAtOnce(windowRows, liveRows, analysisAhead), recordSize),
4230
+ // What it would hold having closed the file and opened it again: every selected column read fresh,
4231
+ // because nothing is cached any more. Higher than `held`, not lower - a cached column this read
4232
+ // selects costs nothing to read now and would cost its area then. Without it the counterfactual
4233
+ // that decides whether releasing helps was answered against the warm figure and said yes too often.
4234
+ afterRelease: freshSymbolBytes === null ? null : this._bytesHeldBy(freshSymbolBytes, this._recordsAtOnce(windowRows, liveRows, analysisAhead), recordSize),
4168
4235
  // A window of so many rows reads so many records at a time, and the pass that reads it ahead of the
4169
4236
  // decode reads the same rows, so the buffer is sized from the rows either way.
4170
4237
  forRows: /* @__PURE__ */ __name((rows) => this._bytesHeldBy(symbolBytes, rows, recordSize), "forRows"),
@@ -4790,7 +4857,8 @@ var init_QvdFileReader = __esm({
4790
4857
  const symbolTableSize = this._symbolTableLength();
4791
4858
  const plan = this._symbolAreaPlan();
4792
4859
  const readSymbolBytes = plan.ranges.reduce((sum, range) => sum + (range.end - range.start), 0);
4793
- const symbolBytes = readSymbolBytes + symbolBytesOf(this._fieldsHeldAfter(fields, allFields).slice(fields.length), symbolTableSize);
4860
+ const retainedBytes = symbolBytesOf(this._fieldsHeldAfter(fields, allFields).slice(fields.length), symbolTableSize);
4861
+ const symbolBytes = readSymbolBytes + retainedBytes;
4794
4862
  const totalRows = headerInteger(this._header["QvdTableHeader"]["NoOfRecords"]);
4795
4863
  const recordSize = headerInteger(this._header["QvdTableHeader"]["RecordByteSize"]);
4796
4864
  validateSymbolTableSize(symbolBytes, this._path, totalRows);
@@ -4804,11 +4872,12 @@ var init_QvdFileReader = __esm({
4804
4872
  fields.length,
4805
4873
  this._materialisesRows,
4806
4874
  liveRows,
4807
- this._bytesHeld(readSymbolBytes, rowsToLoad, recordSize, liveRows, false),
4875
+ this._bytesHeld(readSymbolBytes, rowsToLoad, recordSize, liveRows, false, symbolBytes - retainedBytes),
4808
4876
  // `symbolsToKeep` is non-null exactly when the symbol-usage pass has run, and a pass that has
4809
4877
  // run has read the window's records once already - so the read's total is two passes over them.
4810
4878
  readSymbolBytes + readPasses(symbolsToKeep !== null) * rowsToLoad * recordSize,
4811
- this._symbolCache !== null
4879
+ this._symbolCache !== null,
4880
+ retainedBytes
4812
4881
  );
4813
4882
  }
4814
4883
  warnLargeSymbolTable(
@@ -5081,6 +5150,10 @@ var init_QvdFileReader = __esm({
5081
5150
  const ask = /* @__PURE__ */ __name((asked, rows) => {
5082
5151
  const bytes = symbolBytesOf(this._fieldsHeldAfter(asked, this._allFields), symbolTableLength);
5083
5152
  const read = symbolBytesOf(this._fieldsReadBy(asked), symbolTableLength);
5153
+ const retained = symbolBytesOf(
5154
+ this._fieldsHeldAfter(asked, this._allFields).slice(asked.length),
5155
+ symbolTableLength
5156
+ );
5084
5157
  return checkMemory({
5085
5158
  measured,
5086
5159
  symbolTableSize: bytes,
@@ -5092,7 +5165,8 @@ var init_QvdFileReader = __esm({
5092
5165
  live: liveRows,
5093
5166
  // A paging read keeps whole columns, so it is charged for whole columns - see `estimateMemoryUsage`.
5094
5167
  wholeSymbols: this._symbolCache !== null,
5095
- bytesHeld: this._bytesHeld(read, rows, recordSize, liveRows, analysisAhead),
5168
+ retainedBytes: retained,
5169
+ bytesHeld: this._bytesHeld(read, rows, recordSize, liveRows, analysisAhead, bytes - retained),
5096
5170
  // What it reads from the file, which is not what it holds: the symbol areas it has still to read,
5097
5171
  // and every record the window covers, read a slice at a time and not kept - twice over where the
5098
5172
  // symbol-usage pass will run, since it reads them before the decode reads them again.