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.cjs CHANGED
@@ -2265,9 +2265,10 @@ function recommendedChunkFor(budget, symbolTableSize, windowRows, totalRows, col
2265
2265
  }
2266
2266
  return low;
2267
2267
  }
2268
- function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = null, readBytes = null, wholeSymbols = false) {
2268
+ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePath, safetyFactor = 0.8, columnCount = 0, materialisesRows = true, live = null, bytesHeld = null, readBytes = null, wholeSymbols = false, retainedBytes = 0) {
2269
2269
  const answer = checkMemory({
2270
2270
  wholeSymbols,
2271
+ retainedBytes,
2271
2272
  symbolTableSize,
2272
2273
  maxRows,
2273
2274
  totalRows,
@@ -2300,7 +2301,8 @@ function checkMemory({
2300
2301
  bytesHeld = null,
2301
2302
  readBytes = null,
2302
2303
  measured = null,
2303
- wholeSymbols = false
2304
+ wholeSymbols = false,
2305
+ retainedBytes = 0
2304
2306
  }) {
2305
2307
  if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
2306
2308
  throw new exports.QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", {
@@ -2324,7 +2326,12 @@ function checkMemory({
2324
2326
  rowsLive,
2325
2327
  wholeSymbols
2326
2328
  );
2327
- const { held, forRows: heldForRows, forChunk: heldForChunk } = bytesHeld ?? noBytesHeld;
2329
+ const {
2330
+ held,
2331
+ afterRelease: heldAfterRelease = null,
2332
+ forRows: heldForRows,
2333
+ forChunk: heldForChunk
2334
+ } = bytesHeld ?? noBytesHeld;
2328
2335
  const externalMemory = estimateExternalMemory(liveRows, columnCount) + held;
2329
2336
  const bounded = budget.candidates.map((candidate) => {
2330
2337
  const heapOnly = candidate.source === "V8 heap limit";
@@ -2360,6 +2367,25 @@ function checkMemory({
2360
2367
  const availableMemory = binding ? binding.bytes : budget.bytes;
2361
2368
  const estimatedMemory = binding ? binding.needs : heapMemory;
2362
2369
  const maxAllowedMemory = binding ? binding.allowed : budget.bytes * safetyFactor;
2370
+ const retainedIsTheReason = (() => {
2371
+ if (binding === null || retainedBytes <= 0) {
2372
+ return false;
2373
+ }
2374
+ const heapAfter = estimateMemoryUsage(
2375
+ Math.max(0, symbolTableSize - retainedBytes),
2376
+ maxRows,
2377
+ totalRows,
2378
+ columnCount,
2379
+ materialisesRows,
2380
+ rowsLive,
2381
+ wholeSymbols
2382
+ );
2383
+ const externalAfter = estimateExternalMemory(liveRows, columnCount) + (heldAfterRelease ?? held);
2384
+ return budget.candidates.every((candidate) => {
2385
+ const heapOnly = candidate.source === "V8 heap limit";
2386
+ return (heapOnly ? heapAfter : heapAfter + externalAfter) <= candidate.bytes * safetyFactor;
2387
+ });
2388
+ })();
2363
2389
  if (binding) {
2364
2390
  const includeExternal = !binding.heapOnly;
2365
2391
  const rowsHeldBudget = /* @__PURE__ */ __name((rows) => maxAllowedMemory - (includeExternal ? heldForRows(rows) : 0), "rowsHeldBudget");
@@ -2406,13 +2432,17 @@ function checkMemory({
2406
2432
  const knob = chunked ? "chunkSize" : "limit";
2407
2433
  const recommendedValue = chunked ? recommendedChunk : recommendedMaxRows;
2408
2434
  const nothingFits = recommendedValue === 0;
2435
+ const held2 = retainedIsTheReason;
2436
+ const fixedCost = held2 ? "the columns this file is holding exceed" : "the symbol table alone exceeds";
2437
+ const release = held2 ? `Close the file and open it again to release them, or raise ` : `Raise `;
2438
+ const releaseFirst = held2 ? `Close the file and open it again to release the columns it is holding, which is the direct remedy. ` : "";
2409
2439
  let advice;
2410
2440
  if (nothingFits) {
2411
- 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.`);
2441
+ 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.`);
2412
2442
  } else if (containerBound) {
2413
- 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).`;
2443
+ 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).`;
2414
2444
  } else {
2415
- advice = `Try holding fewer rows using the ${knob} parameter (recommended: ${formatCount(recommendedValue)} rows or less), or raise the heap with --max-old-space-size.`;
2445
+ 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.`;
2416
2446
  }
2417
2447
  const suggestions = [];
2418
2448
  if (!nothingFits) {
@@ -2436,10 +2466,16 @@ function checkMemory({
2436
2466
  exact: symbolTableSize === 0,
2437
2467
  suggestions,
2438
2468
  refusal: {
2439
- 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,
2469
+ 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,
2440
2470
  context: {
2441
2471
  symbolTableSize,
2442
2472
  symbolTableSizeMB: sizeMB,
2473
+ // Whether that figure is the file's symbol table or what an open file is still holding. A
2474
+ // paging read is charged for every column any of its pages decoded and kept, so a caller
2475
+ // branching on the refusal needs to know which of the two it is looking at - the remedies
2476
+ // differ, and for this one releasing is a remedy where raising the limit is only a workaround.
2477
+ holdsDecodedColumns: retainedIsTheReason,
2478
+ retainedSymbolBytes: retainedBytes,
2443
2479
  estimatedMemoryMB: estimatedMB,
2444
2480
  availableMemoryMB: availableMB,
2445
2481
  heapLimitMB,
@@ -2607,19 +2643,39 @@ function validateHeaderStructure(headerObj, filePath, stage) {
2607
2643
  });
2608
2644
  return fieldList;
2609
2645
  }
2610
- function validateSymbolTableSizeEarly(symbolTableLength, filePath) {
2646
+ function validateSymbolTableSizeEarly(symbolTableLength, filePath, retainedBytes = 0) {
2611
2647
  const heapLimit = getHeapLimit();
2612
2648
  const MAX_SYMBOL_TABLE_SIZE = heapLimit * 0.125;
2613
2649
  if (symbolTableLength > MAX_SYMBOL_TABLE_SIZE) {
2614
2650
  const sizeMB = Math.round(symbolTableLength / 1024 / 1024);
2615
2651
  const maxMB = Math.round(MAX_SYMBOL_TABLE_SIZE / 1024 / 1024);
2616
2652
  const heapMB = Math.round(heapLimit / 1024 / 1024);
2653
+ if (retainedBytes > 0 && symbolTableLength - retainedBytes <= MAX_SYMBOL_TABLE_SIZE) {
2654
+ throw new exports.QvdValidationError(
2655
+ `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.`,
2656
+ {
2657
+ file: filePath,
2658
+ symbolTableSize: symbolTableLength,
2659
+ symbolTableSizeMB: sizeMB,
2660
+ holdsDecodedColumns: true,
2661
+ retainedSymbolBytes: retainedBytes,
2662
+ maxAllowed: MAX_SYMBOL_TABLE_SIZE,
2663
+ maxAllowedMB: maxMB,
2664
+ heapLimitMB: heapMB,
2665
+ reason: "memory"
2666
+ }
2667
+ );
2668
+ }
2617
2669
  throw new exports.QvdValidationError(
2618
2670
  `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.`,
2619
2671
  {
2620
2672
  file: filePath,
2621
2673
  symbolTableSize: symbolTableLength,
2622
2674
  symbolTableSizeMB: sizeMB,
2675
+ // Present and false, not absent. A caller told it can branch on this has to find it on both
2676
+ // refusals, or the branch reads `undefined` for the commoner of the two.
2677
+ holdsDecodedColumns: false,
2678
+ retainedSymbolBytes: retainedBytes,
2623
2679
  maxAllowed: MAX_SYMBOL_TABLE_SIZE,
2624
2680
  maxAllowedMB: maxMB,
2625
2681
  heapLimitMB: heapMB,
@@ -4025,6 +4081,10 @@ var init_QvdFileReader = __esm({
4025
4081
  const columnCount = selected.length;
4026
4082
  const symbolBytes = symbolBytesOf(this._fieldsHeldAfter(selected, headerFields), symbolTableLength);
4027
4083
  const readSymbolBytes = symbolBytesOf(this._fieldsReadBy(selected), symbolTableLength);
4084
+ const retainedBytes = symbolBytesOf(
4085
+ this._fieldsHeldAfter(selected, headerFields).slice(selected.length),
4086
+ symbolTableLength
4087
+ );
4028
4088
  const resolved = headerNumbersUsable ? resolveWindow(window, totalRows) : { offset: 0, limit: 0 };
4029
4089
  const windowRows = resolved.limit;
4030
4090
  if (headerNumbersUsable && this._headerMatchesFile) {
@@ -4042,7 +4102,8 @@ var init_QvdFileReader = __esm({
4042
4102
  windowRows,
4043
4103
  recordSize,
4044
4104
  liveRows,
4045
- this._analysisAhead(window, resolved, totalRows, symbolTableLength)
4105
+ this._analysisAhead(window, resolved, totalRows, symbolTableLength),
4106
+ symbolBytes - retainedBytes
4046
4107
  ),
4047
4108
  // What it reads, which is not what it holds - the records go through one buffer and are not
4048
4109
  // kept. Carried so that a refusal's `check` says everything the pre-flight would have said.
@@ -4054,7 +4115,8 @@ var init_QvdFileReader = __esm({
4054
4115
  // A paging read keeps whole columns, so the estimate must not discount its symbols as a window's
4055
4116
  // sample of them - see `estimateMemoryUsage`. Under-charging is the direction that ends in a
4056
4117
  // heap-limit abort rather than an error.
4057
- this._symbolCache !== null
4118
+ this._symbolCache !== null,
4119
+ retainedBytes
4058
4120
  );
4059
4121
  }
4060
4122
  if (window.offset === 0 && window.limit === null) {
@@ -4062,7 +4124,7 @@ var init_QvdFileReader = __esm({
4062
4124
  return;
4063
4125
  }
4064
4126
  const rowsToLoad = windowRows;
4065
- validateSymbolTableSizeEarly(symbolBytes, this._path);
4127
+ validateSymbolTableSizeEarly(symbolBytes, this._path, retainedBytes);
4066
4128
  validateRecordSize(recordSize, this._path, "readData");
4067
4129
  validateRecordCount(totalRows, this._path, "readData");
4068
4130
  const fileBytesRequired = indexTableOffset + (resolved.offset + rowsToLoad) * recordSize;
@@ -4174,9 +4236,14 @@ var init_QvdFileReader = __esm({
4174
4236
  * would hold.
4175
4237
  * @private
4176
4238
  */
4177
- _bytesHeld(symbolBytes, windowRows, recordSize, liveRows, analysisAhead) {
4239
+ _bytesHeld(symbolBytes, windowRows, recordSize, liveRows, analysisAhead, freshSymbolBytes = null) {
4178
4240
  return {
4179
4241
  held: this._bytesHeldBy(symbolBytes, this._recordsAtOnce(windowRows, liveRows, analysisAhead), recordSize),
4242
+ // What it would hold having closed the file and opened it again: every selected column read fresh,
4243
+ // because nothing is cached any more. Higher than `held`, not lower - a cached column this read
4244
+ // selects costs nothing to read now and would cost its area then. Without it the counterfactual
4245
+ // that decides whether releasing helps was answered against the warm figure and said yes too often.
4246
+ afterRelease: freshSymbolBytes === null ? null : this._bytesHeldBy(freshSymbolBytes, this._recordsAtOnce(windowRows, liveRows, analysisAhead), recordSize),
4180
4247
  // A window of so many rows reads so many records at a time, and the pass that reads it ahead of the
4181
4248
  // decode reads the same rows, so the buffer is sized from the rows either way.
4182
4249
  forRows: /* @__PURE__ */ __name((rows) => this._bytesHeldBy(symbolBytes, rows, recordSize), "forRows"),
@@ -4802,7 +4869,8 @@ var init_QvdFileReader = __esm({
4802
4869
  const symbolTableSize = this._symbolTableLength();
4803
4870
  const plan = this._symbolAreaPlan();
4804
4871
  const readSymbolBytes = plan.ranges.reduce((sum, range) => sum + (range.end - range.start), 0);
4805
- const symbolBytes = readSymbolBytes + symbolBytesOf(this._fieldsHeldAfter(fields, allFields).slice(fields.length), symbolTableSize);
4872
+ const retainedBytes = symbolBytesOf(this._fieldsHeldAfter(fields, allFields).slice(fields.length), symbolTableSize);
4873
+ const symbolBytes = readSymbolBytes + retainedBytes;
4806
4874
  const totalRows = headerInteger(this._header["QvdTableHeader"]["NoOfRecords"]);
4807
4875
  const recordSize = headerInteger(this._header["QvdTableHeader"]["RecordByteSize"]);
4808
4876
  validateSymbolTableSize(symbolBytes, this._path, totalRows);
@@ -4816,11 +4884,12 @@ var init_QvdFileReader = __esm({
4816
4884
  fields.length,
4817
4885
  this._materialisesRows,
4818
4886
  liveRows,
4819
- this._bytesHeld(readSymbolBytes, rowsToLoad, recordSize, liveRows, false),
4887
+ this._bytesHeld(readSymbolBytes, rowsToLoad, recordSize, liveRows, false, symbolBytes - retainedBytes),
4820
4888
  // `symbolsToKeep` is non-null exactly when the symbol-usage pass has run, and a pass that has
4821
4889
  // run has read the window's records once already - so the read's total is two passes over them.
4822
4890
  readSymbolBytes + readPasses(symbolsToKeep !== null) * rowsToLoad * recordSize,
4823
- this._symbolCache !== null
4891
+ this._symbolCache !== null,
4892
+ retainedBytes
4824
4893
  );
4825
4894
  }
4826
4895
  warnLargeSymbolTable(
@@ -5093,6 +5162,10 @@ var init_QvdFileReader = __esm({
5093
5162
  const ask = /* @__PURE__ */ __name((asked, rows) => {
5094
5163
  const bytes = symbolBytesOf(this._fieldsHeldAfter(asked, this._allFields), symbolTableLength);
5095
5164
  const read = symbolBytesOf(this._fieldsReadBy(asked), symbolTableLength);
5165
+ const retained = symbolBytesOf(
5166
+ this._fieldsHeldAfter(asked, this._allFields).slice(asked.length),
5167
+ symbolTableLength
5168
+ );
5096
5169
  return checkMemory({
5097
5170
  measured,
5098
5171
  symbolTableSize: bytes,
@@ -5104,7 +5177,8 @@ var init_QvdFileReader = __esm({
5104
5177
  live: liveRows,
5105
5178
  // A paging read keeps whole columns, so it is charged for whole columns - see `estimateMemoryUsage`.
5106
5179
  wholeSymbols: this._symbolCache !== null,
5107
- bytesHeld: this._bytesHeld(read, rows, recordSize, liveRows, analysisAhead),
5180
+ retainedBytes: retained,
5181
+ bytesHeld: this._bytesHeld(read, rows, recordSize, liveRows, analysisAhead, bytes - retained),
5108
5182
  // What it reads from the file, which is not what it holds: the symbol areas it has still to read,
5109
5183
  // and every record the window covers, read a slice at a time and not kept - twice over where the
5110
5184
  // symbol-usage pass will run, since it reads them before the decode reads them again.