Как увеличить лимит данных в new Map()

Я пишу парсер, в строке проверяется один параметр на наличие в (let data = new Map()), но я столкнулся с лимитом примерно в "16777216". (Парсит около 50кк строк.)
Допустим:

let data = new Map();

for (let i = 0; i < 50000000; i++)
{
    try
    {
        data.set(i, true);
    }
    catch (e)
    {
        console.log(`i = ${i}`, e);
        break;
    }
}

Я нашел класс BigMap, но воспользовавшись им у меня уже вылезла другая ошибка:
"FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory"

Как мне увеличить лимит или что еще можно придумать?

[UPD]
Немного изменилась задача по данным. Есть строка:
"Параметр 1:Параметр 2"
Нужно что-бы у "параметр 1", не повторялся "параметр 2", то есть..
Такую строки оно должно пропустить и занести все значения:
"Что-то:Значение 1"
"Что-то:Значение 2"
А у таких только первая строка пройдет, т.к. такое значение уже есть:
"Что-то:Значение 1"
"Что-то:Значение 1"

Это что-то типо:

let data = new Map();
// ...
let params1 = 'Что-то';
let params2 = 'Значение 1';

let values = data.get(params1);

if (data.has(params1) && values.lastIndexOf(params2) !== -1)
{
// bad
return;
}

data.set(params1, [params2, ...values]);

Репозиторий: https://bitbucket.org/Oliver_Patterson/parser/src


Ответы (3 шт):

Автор решения: Дмытрык

Если строки позволяют - сделайте так (после запуска кода, надо немного подождать, даже если выпадет alert)

let data = {}

for (let i = 0; i < 50000000; i++) {
  try {
    data[i] = true;
  } catch (e) {
    console.log(`i = ${i}`, e);
    break;
  }
}
console.log(Object.keys(data).length);

Эффект тот же. Но луче разбить на чанки, как советовали в комментах

→ Ссылка
Автор решения: vsemozhebuty

Насколько я понимаю, ваша проблема не в размере входных данных (размере файла или строки), а в размере промежуточной структуры данных, по которой вы проверяете наличие повторяющихся строк.

Можно попробовать создание нескольких Map в массиве: при достижении лимита ключей создавать новый подраздел, а проверять сразу по всем разделам в массиве.

Только нужно учесть, что по умолчанию Node.js имеет лимит в 2 GB памяти. Когда я тестировал свой пример ниже, создавая ключи-числа и присваивая им true, как в вашем примере, мне этой памяти хватило. Но как только я стал использовать ключи-строки и присваивать им такие же числа, памяти уже не хватило. Увеличить память можно так (число означает мегабайты):

node --max_old_space_size=4096 test.mjs

Возможно, это поможет вам решить проблему с BigMap, если мой вариант с несколькими Map вам не подойдёт.

Запускайте этот пример в браузерах с осторожностью, я не знаю, какой где лимит памяти и что будет, если его исчерпать в разных браузерах:

const mapKeysLimit = 2 ** 24;
const data = [new Map()];

for (let i = 0; i < 50000000; i++) {
  let currentMap = data[data.length - 1];
  if (currentMap.size === mapKeysLimit) {
    currentMap = new Map();
    data.push(currentMap);
    console.log(`New map when i = ${i}.`);
  }
  currentMap.set(`${i}`, i);
}

console.log(data.map(map => map.size));

function checkInMaps(maps, key) {
  return maps.some(map => map.has(key));
}

function findInMaps(maps, key) {
  for (const map of maps) {
    const value = map.get(key);
    if (value !== undefined) return value;
  }
  return undefined;
}

console.log(checkInMaps(data, '1'));
console.log(findInMaps(data, '1'));

console.log(checkInMaps(data, '50000001'));
console.log(findInMaps(data, '50000001'));

Вывод у меня в Node.js:

node --max_old_space_size=4096 test.mjs

New map when i = 16777216.
New map when i = 33554432.
[ 16777216, 16777216, 16445568 ]
true
1
false
undefined
→ Ссылка
Автор решения: nörbörnën

Решение этой задачи с использованием встраиваемой базы данных для хранения уникальных пар.

Такое решение не требует костылей увеличения размера доступной памяти и более перспективно, если приложение получит дальнейшее развитие - работать с базой данных проще, чем при каждом запуске приложения заново считывать в память огромный массив данных.

/* eslint-disable no-use-before-define, implicit-arrow-linebreak, no-plusplus, max-len */
// @ts-check
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const sqlite3 = require('sqlite3').verbose();

const db = new sqlite3.Database(':memory:');
// const databasefile = path.join(__dirname, 'sqlite.db');
// const db = new sqlite3.Database(databasefile);

db.serialize(async () => {
  await initDatabase();

  const filepath = path.join(__dirname, 'generator', 'data.txt');
  const res = await handleFile(filepath);

  console.log(JSON.stringify(res, null, 2));
  console.log(`Время выполнения скрипта: ${res.time}ms`);

  await new Promise((resolve) => db.close(resolve));
});

/**
 * @returns {Promise<void>}
 */
async function initDatabase() {
  await new Promise((resolve, reject) =>
    db.run('pragma journal_mode = MEMORY;', (err, ...args) => (err ? reject(err) : resolve(...args))));

  await new Promise((resolve, reject) =>
    db.run('CREATE TABLE pairs (key TEXT, value TEXT)', (err, ...args) => (err ? reject(err) : resolve(...args))));

  await new Promise((resolve, reject) =>
    db.run('create unique index idx_pair_uniq on pairs (key, value)', (err, ...args) => (err ? reject(err) : resolve(...args))));
}

/**
 * @param {string} filepath
 * @returns {Promise<{lines: number; doubles: number; uniq_keys: number; uniq_pairs: number; time: number; }>}
 */
async function handleFile(filepath) {
  const timeBegin = Date.now();

  const result = {
    lines: 0,
    doubles: 0,
    uniq_keys: 0,
    uniq_pairs: 0,
  };

  const stream = fs.createReadStream(filepath);

  // @ts-ignore
  const rl = readline.createInterface({
    input: stream,
    output: process.stdout,
    crlfDelay: Infinity,
    terminal: false,
  });

  for await (const line of rl) {
    if (line !== undefined && line !== null) {
      result.lines++;

      const pair = await handleLine(line);

      if (pair) result.uniq_pairs++;
      if (pair === null) result.doubles++;
    }
  }

  rl.close();

  const { n } = await new Promise((resolve, reject) =>
    db.get('SELECT count(distinct(key)) as n FROM pairs', (err, ...args) => (err ? reject(err) : resolve(...args))));

  result.uniq_keys = n;

  return { ...result, time: (Date.now() - timeBegin) };
}

/**
 * @param {string} line
 * @returns {Promise<{ key: string; value: string; } | null>}
 */
async function handleLine(line) {
  const [key, value] = line.replace(/^\s+|\s+$/g, '').split(/: /);

  try {
    await validatePair(key, value);
    return { key, value };
  } catch (err) {
    if (err && err.code === 'SQLITE_CONSTRAINT') {
      return null;
    }
    console.error(err);
  }
}

/**
 * @type { import('sqlite3').Statement }
 */
let pairInsertStatement;

/**
 * @param {string} key
 * @param {string} value
 * @returns {Promise<void>}
 */
async function validatePair(key, value) {
  if (!pairInsertStatement) {
    pairInsertStatement = db.prepare('INSERT INTO pairs (key, value) values (?, ?)');
  }

  return new Promise((resolve, reject) => pairInsertStatement.run(
    [key, value],
    (err, ...args) => (err ? reject(err) : resolve(...args))
  ));
}

Запуск обработки файла с 60000000 записями, вывод в консоль:

{
  "lines": 60000006,
  "doubles": 18000940,
  "uniq_keys": 11999063,
  "uniq_pairs": 41999066,
  "time": 3684828
}
Время выполнения скрипта: 01:01:24.828

Скрипт для генерации тестового набора данных:

const crypto = require('crypto');
const fs = require('fs');

const fd = fs.openSync(`${__dirname}/data.txt`, 'w+');
for (const str of generate(20000000)) {
  fs.appendFileSync(fd, str);
  fs.appendFileSync(fd, '\r\n');
}
fs.fsyncSync(fd);
for (const str of generate(20000000)) {
  fs.appendFileSync(fd, str);
  fs.appendFileSync(fd, '\r\n');
}
fs.fsyncSync(fd);
for (const str of generate(20000000)) {
  fs.appendFileSync(fd, str);
  fs.appendFileSync(fd, '\r\n');
}
fs.closeSync(fd);


/**
 * @param { number } size
 * @returns { string[] }
 */
function generate(size = 50 * 1000 * 1000) {
  let array = [];
  while (array.length < size) {
    const key = crypto.randomBytes(randomNumber(14, 25))
      .toString('base64').replace(/={1,}$/, '');

    for (let i = 0; i < randomNumber(3, 5); i++) {
      const value = crypto.randomBytes(randomNumber(4, 8)).toString('base64');
      const line = `${key}: ${value}`;
      array.push(line);
      if (i % 2 !== 0) {
        array.push(line);
      }
    }
    if (array.length >= 1000000 && array.length % 1000000 < 10) {
      console.log(array.length);
    }
  }
  array = shuffle(array);
  return array;
}

/**
 * @param {number} [min=0]
 * @param {number} [max]
 * @returns {number}
 */
function randomNumber(min = 0, max) {
  if (max === null || max === undefined) {
    max = min; min = 0;
  }
  return Math.trunc(Math.random() * (max - min) + min);
}

/**
 * @param { any[] } array
 * @returns { any[] }
 */
function shuffle(array) {
  let currentIndex = array.length;
  let temporaryValue;
  let randomIndex;

  while (currentIndex !== 0) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}

$ node --max-old-space-size=7168 src/stream-big-data-1/generator/index.js
→ Ссылка