Утечка памяти в 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 может сделать это автоматически для вас, но, возможно, нет.

Некоторые связанные с этим вопросы: этот, тот.

Другие вопросы по тегам