qvdjs 0.9.2 → 0.9.3

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
@@ -734,6 +734,28 @@ var init_bitUtils = __esm({
734
734
  function getHeapLimit() {
735
735
  return v8.getHeapStatistics().heap_size_limit;
736
736
  }
737
+ function heapLimitIsMeaningful() {
738
+ return !process.versions.bun && !process.versions.deno;
739
+ }
740
+ function getMemoryBudget() {
741
+ const candidates = [];
742
+ if (heapLimitIsMeaningful()) {
743
+ candidates.push({ source: "V8 heap limit", bytes: getHeapLimit() });
744
+ }
745
+ const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : 0;
746
+ if (constrained > 0 && constrained < os.totalmem()) {
747
+ candidates.push({ source: "container memory limit", bytes: constrained });
748
+ }
749
+ if (candidates.length === 0) {
750
+ candidates.push({ source: "total system memory", bytes: os.totalmem() });
751
+ }
752
+ const observed = [{ source: "free memory (os.freemem)", bytes: os.freemem() }];
753
+ if (typeof process.availableMemory === "function") {
754
+ observed.push({ source: "available memory", bytes: process.availableMemory() });
755
+ }
756
+ const binding = candidates.reduce((lowest, candidate) => candidate.bytes < lowest.bytes ? candidate : lowest);
757
+ return { bytes: binding.bytes, limitedBy: binding.source, candidates, observed };
758
+ }
737
759
  function estimateMemoryUsage(symbolTableSize, maxRows, totalRows) {
738
760
  const FULL_PARSE_OVERHEAD = 6;
739
761
  const MINIMAL_OVERHEAD = 0.01;
@@ -750,11 +772,14 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
750
772
  if (typeof safetyFactor !== "number" || safetyFactor < 0 || safetyFactor > 1) {
751
773
  throw new QvdValidationError("safetyFactor must be a number between 0.0 and 1.0", { safetyFactor });
752
774
  }
753
- const availableMemory = os.freemem();
775
+ if (safetyFactor === 0) {
776
+ return;
777
+ }
778
+ const budget = getMemoryBudget();
754
779
  const heapLimit = getHeapLimit();
780
+ const availableMemory = budget.bytes;
755
781
  const estimatedMemory = estimateMemoryUsage(symbolTableSize, maxRows, totalRows);
756
- const effectiveLimit = Math.min(availableMemory, heapLimit);
757
- const maxAllowedMemory = effectiveLimit * safetyFactor;
782
+ const maxAllowedMemory = budget.bytes * safetyFactor;
758
783
  if (estimatedMemory > maxAllowedMemory) {
759
784
  const safeSymbolPercentage = maxAllowedMemory / (symbolTableSize * 6);
760
785
  const safeRowPercentage = Math.pow(safeSymbolPercentage, 2);
@@ -764,9 +789,12 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
764
789
  const availableMB = Math.round(maxAllowedMemory / 1024 / 1024);
765
790
  const heapLimitMB = Math.round(heapLimit / 1024 / 1024);
766
791
  const availableRamMB = Math.round(availableMemory / 1024 / 1024);
767
- const limitingFactor = heapLimit < availableMemory ? "V8 heap limit" : "available RAM";
792
+ const limitingFactor = budget.limitedBy;
793
+ const budgetBreakdown = budget.candidates.map((candidate) => `${candidate.source} ${Math.round(candidate.bytes / 1024 / 1024)}MB`).join(", ");
794
+ const observedBreakdown = budget.observed.map((entry) => `${entry.source} ${Math.round(entry.bytes / 1024 / 1024)}MB`).join(", ");
795
+ const advice = limitingFactor === "container memory limit" ? `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 load fewer rows with maxRows (recommended: ${recommendedMaxRows.toLocaleString()} rows or less).` : `Try loading fewer rows using the maxRows parameter (recommended: ${recommendedMaxRows.toLocaleString()} rows or less), or raise the heap with --max-old-space-size.`;
768
796
  throw new QvdValidationError(
769
- `Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}: ${heapLimitMB}MB heap / ${availableRamMB}MB RAM). Try loading fewer rows using the maxRows parameter (recommended: ${recommendedMaxRows.toLocaleString()} rows or less).`,
797
+ `Insufficient memory to load file safely. Symbol table: ${sizeMB}MB, Estimated memory needed: ${estimatedMB}MB, Available: ${availableMB}MB (limited by ${limitingFactor}; considered: ${budgetBreakdown}; observed but not used: ${observedBreakdown}). ` + advice,
770
798
  {
771
799
  file: filePath,
772
800
  symbolTableSize,
@@ -776,6 +804,8 @@ function validateMemoryAvailability(symbolTableSize, maxRows, totalRows, filePat
776
804
  heapLimitMB,
777
805
  availableRamMB,
778
806
  limitingFactor,
807
+ memoryBudget: budget.candidates,
808
+ memoryObserved: budget.observed,
779
809
  totalRows,
780
810
  maxRows,
781
811
  recommendedMaxRows
@@ -1289,14 +1319,22 @@ var init_QvdFileReader = __esm({
1289
1319
  * points outside it is rejected. Defaults to the current working directory. To permit
1290
1320
  * an entire volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or
1291
1321
  * empty value falls back to the working directory rather than removing the restriction.
1292
- * @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines
1293
- * what percentage of available memory (or V8 heap limit, whichever is smaller) can be used
1294
- * for loading QVD files. Default is 0.3 (30%). Increase for larger heap configurations.
1322
+ * @param {number} [options.memorySafetyFactor=0.3] Fraction (0.0-1.0) of the memory budget a
1323
+ * load may use. The budget is the smallest of the V8 heap limit, any container memory limit,
1324
+ * and the memory the OS reports as available. Default is 0.3. **Zero disables the memory
1325
+ * check entirely**, which is the escape hatch for runtimes whose limits cannot be measured -
1326
+ * Bun reports its current heap as its heap limit - and for callers who would rather manage
1327
+ * memory themselves than trust the estimate.
1328
+ * @param {number} [options.symbolFilteringThreshold=52428800] Symbol table size, in bytes,
1329
+ * above which a lazy load switches to the two-pass filtering path. The default of 50MB is
1330
+ * the point where the extra analysis pass pays for itself; lower it to use filtering on
1331
+ * smaller files, raise it to keep the simpler single-pass read for longer.
1295
1332
  */
1296
1333
  constructor(filePath, options = {}) {
1297
- const { allowedDir, memorySafetyFactor = 0.3 } = options;
1334
+ const { allowedDir, memorySafetyFactor = 0.3, symbolFilteringThreshold = 50 * 1024 * 1024 } = options;
1298
1335
  this._path = validatePath(filePath, allowedDir);
1299
1336
  this._memorySafetyFactor = memorySafetyFactor;
1337
+ this._symbolFilteringThreshold = symbolFilteringThreshold;
1300
1338
  this._buffer = null;
1301
1339
  this._headerOffset = null;
1302
1340
  this._symbolTableOffset = null;
@@ -1728,15 +1766,12 @@ var init_QvdFileReader = __esm({
1728
1766
  await this._readData(maxRows);
1729
1767
  await this._parseHeader();
1730
1768
  let symbolsToKeep = null;
1769
+ let symbolsKept = null;
1731
1770
  if (maxRows !== null && this._header) {
1732
1771
  const symbolTableLength = parseInt(this._header["QvdTableHeader"]["Offset"], 10);
1733
- const SYMBOL_FILTERING_THRESHOLD = 50 * 1024 * 1024;
1734
- if (symbolTableLength > SYMBOL_FILTERING_THRESHOLD) {
1772
+ if (symbolTableLength > this._symbolFilteringThreshold) {
1735
1773
  symbolsToKeep = await this._analyzeIndexTableSymbolUsage(maxRows);
1736
- const totalSymbols = Array.from(symbolsToKeep.values()).reduce((sum, set) => sum + set.size, 0);
1737
- console.log(
1738
- `[Phase 2.5 Optimization] Using stream-and-skip parsing: keeping ${totalSymbols} symbols from ${(symbolTableLength / 1024 / 1024).toFixed(1)}MB symbol table`
1739
- );
1774
+ symbolsKept = Array.from(symbolsToKeep.values()).reduce((sum, set) => sum + set.size, 0);
1740
1775
  }
1741
1776
  }
1742
1777
  await this._parseSymbolTable(symbolsToKeep, maxRows);
@@ -1773,7 +1808,14 @@ var init_QvdFileReader = __esm({
1773
1808
  const columns = fields.map((field) => field["FieldName"]);
1774
1809
  const data = this._indexTable.map((_, index) => getRow(index));
1775
1810
  const metadata = this._header["QvdTableHeader"];
1776
- return new QvdDataFrame(data, columns, metadata);
1811
+ const loadStats = {
1812
+ symbolTableBytes: parseInt(this._header["QvdTableHeader"]["Offset"], 10),
1813
+ totalRows: parseInt(this._header["QvdTableHeader"]["NoOfRecords"], 10),
1814
+ rowsLoaded: data.length,
1815
+ symbolFiltering: symbolsToKeep !== null,
1816
+ symbolsKept
1817
+ };
1818
+ return new QvdDataFrame(data, columns, metadata, loadStats);
1777
1819
  }
1778
1820
  };
1779
1821
  }
@@ -1790,11 +1832,13 @@ var init_QvdDataFrame = __esm({
1790
1832
  * @param {Array<Array<any>>} data The data of the data frame.
1791
1833
  * @param {Array<string>} columns The columns of the data frame.
1792
1834
  * @param {QvdMetadata|null} metadata The metadata from the QVD file header (optional).
1835
+ * @param {QvdLoadStats|null} loadStats Statistics about the read (optional).
1793
1836
  */
1794
- constructor(data, columns, metadata = null) {
1837
+ constructor(data, columns, metadata = null, loadStats = null) {
1795
1838
  this._data = data;
1796
1839
  this._columns = columns;
1797
1840
  this._metadata = metadata;
1841
+ this._loadStats = loadStats;
1798
1842
  }
1799
1843
  /**
1800
1844
  * Returns the data of the data frame.
@@ -1821,6 +1865,21 @@ var init_QvdDataFrame = __esm({
1821
1865
  get metadata() {
1822
1866
  return this._metadata;
1823
1867
  }
1868
+ /**
1869
+ * Returns statistics about the read that produced this data frame.
1870
+ *
1871
+ * Only a frame returned by fromQvd() carries these; fromDict(), head() and tail() produce
1872
+ * frames that describe no particular read, and report null rather than a stale figure.
1873
+ *
1874
+ * The main use is confirming that a lazy load actually filtered the symbol table:
1875
+ * `symbolFiltering` says whether the two-pass path ran, and `symbolsKept` how many symbols
1876
+ * survived it, which for a small maxRows should be a tiny fraction of the file's total.
1877
+ *
1878
+ * @return {QvdLoadStats|null} Load statistics, or null if this frame did not come from a file.
1879
+ */
1880
+ get loadStats() {
1881
+ return this._loadStats;
1882
+ }
1824
1883
  /**
1825
1884
  * Returns file-level metadata from the QVD header.
1826
1885
  * @return {Object} File-level metadata properties.
@@ -2196,9 +2255,12 @@ var init_QvdDataFrame = __esm({
2196
2255
  * outside it is rejected. Defaults to the current working directory. To permit an entire
2197
2256
  * volume, pass its root explicitly ('/' on POSIX, 'C:\\' on Windows); a null or empty value falls
2198
2257
  * back to the working directory rather than removing the restriction.
2199
- * @param {number} [options.memorySafetyFactor=0.3] Memory safety factor (0.0-1.0). Determines what percentage
2200
- * of available memory (or V8 heap limit, whichever is smaller) can be used. Default is 0.3 (30%).
2201
- * Increase this (e.g., to 0.5 or 0.7) when running with larger heap sizes via --max-old-space-size.
2258
+ * @param {number} [options.memorySafetyFactor=0.3] Fraction (0.0-1.0) of the memory budget a load
2259
+ * may use. The budget is the smallest of the V8 heap limit, any container memory limit, and the
2260
+ * memory the OS reports as available. Default is 0.3; raise it when running with a larger heap via
2261
+ * --max-old-space-size. **Zero disables the memory check entirely.**
2262
+ * @param {number} [options.symbolFilteringThreshold=52428800] Symbol table size, in bytes, above which
2263
+ * a lazy load switches to the two-pass filtering path. Defaults to 50MB.
2202
2264
  * @throws {QvdValidationError} If options.maxRows is neither null/undefined nor a non-negative integer.
2203
2265
  * @return {Promise<QvdDataFrame>} The data frame of the QVD file.
2204
2266
  */
@@ -2206,7 +2268,8 @@ var init_QvdDataFrame = __esm({
2206
2268
  const { QvdFileReader: QvdFileReader2 } = await Promise.resolve().then(() => (init_QvdFileReader(), QvdFileReader_exports));
2207
2269
  const readerOptions = {
2208
2270
  allowedDir: options.allowedDir,
2209
- memorySafetyFactor: options.memorySafetyFactor
2271
+ memorySafetyFactor: options.memorySafetyFactor,
2272
+ symbolFilteringThreshold: options.symbolFilteringThreshold
2210
2273
  };
2211
2274
  return await new QvdFileReader2(path3, readerOptions).load(options.maxRows !== void 0 ? options.maxRows : null);
2212
2275
  }