Утечка памяти в JavaScript
Я пытаюсь написать парсер для STEP-файлов в javascript, который будет использоваться в основном в браузере, но также и в Node, а сейчас я использую Node для отладки.
Это идет довольно хорошо, и это анализирует некоторое время. Но когда я получаю действительно большие файлы с миллионами строк (около 200 МБ и более), он задыхается и, в конце концов, вылетает и жалуется на кучу JavaScript из памяти!
Файлы выглядят примерно так:
...
#10=ORGANIZATION('O0001','LKSoft','company');
#11=PRODUCT_DEFINITION_CONTEXT('part definition',#12,'manufacturing');
#12=APPLICATION_CONTEXT('mechanical design');
#13=APPLICATION_PROTOCOL_DEFINITION('','automotive_design',2003,#12);
#14=PRODUCT_DEFINITION('0',$,#15,#11);
#15=PRODUCT_DEFINITION_FORMATION('1',$,#16);
#16=PRODUCT('A0001','Test Part 1','',(#18));
#17=PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#16));
#18=PRODUCT_CONTEXT('',#12,'');
...
#3197182=APPLIED_ORGANIZATION_ASSIGNMENT(#10,#20,(#16));
#3197183=ORGANIZATION_ROLE('id owner');
Файлы немного нерегулярные, поэтому я пишу довольно тупой парсер, разбирая букву за буквой:
const fs = require('fs');
class bigObject {
constructor(data) {
this.parse(data);
}
propertyLexer(row) {
let refNrRE = /[-0-9]/;
let floatNumberRE = /[.\-0-9E]/;
let charsRE = /[_a-zA-Z.]/;
let stringRE = /'((?:''|[^'])*)'/;
let lexedRow = [];
let current = 0;
let rowLen = row.length;
while (current < rowLen) {
let char = row[current];
// I.E. #32123
if (char === '#') {
let property = '';
while (refNrRE.test(row[current + 1]) && current < rowLen) {
current++;
property += row[current];
}
lexedRow.push(parseInt(property));
current++;
}
// Empty property
else if (char === '$') {
lexedRow.push('');
current++;
}
// Skip to next property
else if (char === ',') {
current++;
}
// I.E. 'Comments, blabla (more comments)'
else if (char === "'") {
let property = stringRE.exec(row.substr(current));
lexedRow.push(property[1]);
current += property[1].length + 2;
}
// I.E. .AREAUNIT.
else if (charsRE.test(char)) {
let property = '';
while (charsRE.test(row[current]) && current < rowLen) {
property += row[current];
current++;
}
lexedRow.push(property);
}
// I.E. -1000.00
else if (floatNumberRE.test(char)) {
let property = '';
while (floatNumberRE.test(row[current]) && current < rowLen) {
property += row[current];
current++;
}
lexedRow.push(property);
}
// Skip rest for now
else {
current++;
}
}
return lexedRow;
}
parse(data) {
if (typeof data !== "string") {
try {
data = data.toString();
}
catch (e) {
throw `Indata not string or not able to convert to string: ${e}`;
}
}
let stepRowRE = /#\d+\s*=\s*[a-zA-Z0-9]+\s*\([^)]*(?:\)(?!;)[^)]*)*\);/g;
// Split single row into three capture groups
let singleRowWithGroupingRE = /^#(\d+)\s*=\s*([a-zA-Z0-9]+)\s*\(([^)]*(?:\)(?!;)[^)]*)*)\);/;
let stepRows = data.match(stepRowRE);
let rowIndex = stepRows.length - 1;
let rowsFromFile = {};
let count = 0;
for (let i = 0; i <= rowIndex; i++) {
let matching = singleRowWithGroupingRE.exec(stepRows[i]);
rowsFromFile[matching[1]] = {c: matching[2], p: this.propertyLexer(matching[3].replace(/(\r\n|\n|\r)/gm, ''))};
if (i % 200000 === 0) {
console.log(i + '::' + JSON.stringify(rowsFromFile[matching[1]]));
}
count++;
}
}
}
//// Start here ////
fs.readFile('./ifc-files/A-40-V-00252.ifc', (err, data) => {
let newObject = new bigObject(data);
});
Я получаю эту ошибку:
<--- Last few GCs --->
[11348:000002D4A6E72260] 81407 ms: Mark-sweep 1403.2 (1458.8) ->
1403.2 (1458.8) MB, 2428.1 / 0.0 ms allocation failure GC in old space requested [11348:000002D4A6E72260] 83836 ms: Mark-sweep
1403.2 (1458.8) -> 1403.2 (1428.8) MB, 2429.0 / 0.0 ms last resort gc [11348:000002D4A6E72260] 86282 ms: Mark-sweep 1403.2 (1428.8) ->
1403.1 (1428.8) MB, 2446.3 / 0.0 ms last resort gc
<--- JS stacktrace --->
==== JS stack trace =========================================
Security context: 00000384656C0D51 <JS Object>
1: parse [C:\Users\user\Projects\parser\index.js:~95] [pc=000000525FB71B18](this=000001EE5F96DE19 <a bigObject with map 0000036221B1B7A9>,data=0000034357F04201 <Very long string[190322237]>)
2: new bigObject [C:\Users\user\Projects\parser\index.js:8] [pc=000000525FB48737](this=000001EE5F96DE19 <a bigObject with map 0000036221B1B7...
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
Я уже несколько дней пытаюсь найти причину этого, но не вижу ничего похожего на утечку памяти или бесконечный цикл.
Моя машина имеет 16 ГБ памяти и должна легко справиться с файлом 200 МБ, много раз!
Есть ли кто-нибудь, кто может помочь мне с моей проблемой? Спасибо!
РЕДАКТИРОВАТЬ: все работает очень хорошо, если я использую Firefox или даже Edge(!), А также когда я использую --max_old_space_size=4096
флаг для увеличения доступной памяти для Chrome/Node (V8). Но маловероятно, что обычные пользователи будут делать это... Поэтому мне все еще нужно сделать его более эффективным с точки зрения памяти. Но я понятия не имею, как.
РЕДАКТИРОВАТЬ 2: Это не JSON.stringify или факт, что я прочитал весь файл, который вызывает проблему. Это будет проблемой, если я попытаюсь прочитать файл большего размера, чем сейчас. Но сейчас это больше, потому что я храню слишком много в памяти или что-то в этом роде.
1 ответ
Ваше приложение аварийно завершает работу, прежде чем приступить к чему-либо сложному: сбой в строке 95 происходит при вызове data.toString().
Очевидно, Node.js не любит строки размером 200 МБ. Это не особенно удивительно; 200 МБ - это много, что нужно для любой реализации String.
Поскольку ваш входной файл состоит из записей, разделенных новой строкой, я думаю, что предложение от mscdex - правильный путь: используйте readline, читайте файл построчно и анализируйте каждую строку.
Этот пример кода, кажется, делает то, что вы хотите.
Строковый подход имеет дополнительное преимущество, заключающееся в том, что он не блокирует цикл обработки событий. Вместо выполнения одной огромной задачи без какой-либо возможности чередовать другие события, вы можете легко структурировать свое приложение так, чтобы оно доходило между каждым линейным событием. readline
может сделать это автоматически для вас, но, возможно, нет.