Как увеличить лимит данных в 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);
Эффект тот же. Но луче разбить на чанки, как советовали в комментах
Насколько я понимаю, ваша проблема не в размере входных данных (размере файла или строки), а в размере промежуточной структуры данных, по которой вы проверяете наличие повторяющихся строк.
Можно попробовать создание нескольких 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
Решение этой задачи с использованием встраиваемой базы данных sqlite для хранения уникальных пар.
Такое решение не требует костылей увеличения размера доступной памяти и более перспективно, если приложение получит дальнейшее развитие - работать с базой данных проще, чем при каждом запуске приложения заново считывать в память огромный массив данных.
/* 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