Node.js утечка памяти при чтении и записи больших файлов - PullRequest
0 голосов
/ 06 марта 2020

В настоящее время я пытаюсь реализовать метод построения индекса SPIMI в Node, и я столкнулся с проблемой.

Код выглядит следующим образом:

let fs = require("fs");
let path = require("path");

module.exports = {

    fileStream: function (dirPath, fileStream) {
        return buildFileStream(dirPath, fileStream);
    },

    buildSpimi: function (fileStream, outDir) {
        let invIndex = {};
        let sortedInvIndex = {};
        let fileNameCount = 1;
        let outputTXT = "";
        let entryCounter = 0;
        let resString = "";
        fileStream.forEach((filePath, fileIndex) => {
            let data = fs.readFileSync(filePath).toString('utf-8');
            data = data.toUpperCase().split(/[^a-zA-Z]/).filter(function (ch) { return ch.length != 0; });
            data.forEach(token => {
                //CHANGE THE SIZE IF NECESSARY (4e+?)
                if (entryCounter > 100000) {
                    Object.keys(invIndex).sort().forEach((key) => {
                        sortedInvIndex[key] = invIndex[key];
                    });
                    outputTXT = outDir + "block" + fileNameCount;
                    for (let SItoken in sortedInvIndex) {
                        resString += SItoken + "," + sortedInvIndex[SItoken].toString();
                    };
                    fs.writeFile(outputTXT, resString, (err) => { if (err) console.log(error); });
                    resString = "";
                    entryCounter = 0;
                    sortedInvIndex = {};
                    invIndex = {};
                    console.log(outputTXT + " - written;");
                    fileNameCount++;
                };
                if (invIndex[token] == undefined) {
                    invIndex[token] = [];
                    entryCounter++;
                };
                if (!invIndex[token].includes(fileIndex)) {
                    invIndex[token].push(fileIndex);
                    entryCounter++;
                };
            });
        });
        Object.keys(invIndex).sort().forEach((key) => {
            sortedInvIndex[key] = invIndex[key];
        });
        outputTXT = outDir + "block" + fileNameCount;
        for (let SItoken in sortedInvIndex) {
            resString += SItoken + "," + sortedInvIndex[SItoken].toString();
        };
        fs.writeFile(outputTXT, resString, (err) => { if (err) console.log(error); });
        console.log(outputTXT + " - written;");
    }

}

function buildFileStream(dirPath, fileStream) {
    fileStream = fileStream || 0;
    fs.readdirSync(dirPath).forEach(function (file) {
        let filepath = path.join(dirPath, file);
        let stat = fs.statSync(filepath);
        if (stat.isDirectory()) {
            fileStream = buildFileStream(filepath, fileStream);
        } else {
            fileStream.push(filepath);
        }
    });
    return fileStream;
}

Я использую экспортированные функции в отдельном файле:

let spimi = require("./spimi");
let outputDir = "/Users/me/Desktop/SPIMI_OUT/"
let inputDir = "/Users/me/Desktop/gutenberg/2/2";

fileStream = [];
let result = spimi.fileStream(inputDir, fileStream);
console.table(result)
console.log("Finished building the filestream");

let t0 = new Date();
spimi.buildSpimi(result, outputDir);
let t1 = new Date();

console.log(t1 - t0);

Хотя этот код работает при попытке сравнительно небольших объемов данных (я тестировал до 1,5 ГБ), очевидно, что где-то есть утечка памяти, как при мониторинге ОЗУ Я могу видеть, что он увеличивается до 4-5 ГБ).

Я потратил довольно много времени, пытаясь выяснить, в чем может быть причина, но я все еще не мог найти проблему.

Буду признателен за любые намеки на это! Спасибо!

Ответы [ 2 ]

0 голосов
/ 06 марта 2020

Михаил, добавляя к тому, что сказал друг, на самом деле это не утечка памяти. Он работает так, как задумано.

Следует учитывать, что readFile буферизирует весь файл ! Это приведет к огромному раздутию памяти. Лучшей альтернативой является реализация fs.createReadStream(), которая будет буферизовать только ту часть файла, которую вы сейчас читаете. К сожалению, реализация этого решения может потребовать полного переписывания вашего кода, поскольку он возвращает fs.ReadStream, который не будет вести себя так, как вы сейчас работаете с файлами Проверьте эту ссылку и прочитайте нижнюю часть раздела, чтобы увидеть, что я ' м ссылки

0 голосов
/ 06 марта 2020

Что нужно понять о языке и сборке мусора в целом, так это:

data = data.toUpperCase().split(/[^a-zA-Z]/).filter(...)

создает три дополнительных копий ваших данных. Во-первых, заглавная копия. Затем разделить массив массива. Затем отфильтрованная копия разделенного массива.

Итак, на данный момент у вас есть четыре копии ваших данных в памяти. Все, но отфильтрованный массив теперь пригоден для сборки мусора, когда G C получает возможность запуска, но если эти данные изначально были большими, вы будете использовать как минимум в 3x-4x больше памяти, чем размер файла (в зависимости от того, сколько элементов массива удалено в вашей операции .filter()).

Ничто из этого не является утечкой, но это очень большое пик использование памяти что может быть проблемой.

Более эффективный способ обработки больших файлов - это обрабатывать их как поток (не считывать их все в память сразу). Вы читаете порцию небольшого размера (скажем, 1024 байта), обрабатываете ее, читаете порцию, обрабатываете, одновременно следя за границами порции. Если ваш файл, естественно, имеет границы строк, уже есть готовые решения для обработки строк за строкой. Если нет, вы можете создать свой собственный механизм обработки чанка. Нам нужно посмотреть образец ваших данных, чтобы сделать более конкретные c предложения по обработке чанков.


В качестве еще одного момента, если у вас будет много ключей в invIndex, тогда это Строка кода начинает становиться неэффективной, и вы делаете это в вашем l oop:

Object.keys(invIndex).sort()

Это берет ваш объект и получает все ключи во временном массиве, который вы используете только для целей обновление sortedInvIndex, которое является еще одной копией ваших данных. Итак, прямо здесь, этот набор кода создает три копии всех ваших ключей и две копии всех значений. И он делает это каждый раз через ваш l oop. Опять же, много пиковых использования памяти, которое G C обычно не очищает, пока ваша функция не будет выполнена.


Редизайн для способ обработки этих данных может снизить пиковое использование памяти в 100 раз. Для эффективности памяти вы хотите, чтобы только начальные данные, окончательное представление данных, а затем немного больше, использовались для временных преобразований, которые использовались одновременно. Вы никогда не хотите обрабатывать все данные несколько раз, потому что каждый раз, когда вы это делаете, создается еще одна полная копия всех данных, которая способствует пиковому использованию памяти.


Если вы показываете как выглядит ввод данных и какова структура данных, которую вы пытаетесь получить, я мог бы, вероятно, попробовать гораздо более эффективную реализацию.

...