В интернете опять кто-то не прав – во вчерашнем Node Weekly была ссылка на пост в котором автор пытается измерить и сравнить с "аналогами" производительность Stream API в Node.js. Грусть вызывает, то как автор работает со стримами и какие он выводы он пытается на основе этого сделать:
...this worked pretty well on smaller files, but once I got to the biggest file, the same error happened. Although Node.js was streaming the inputs and outputs, it still attempted to hold the whole file in memory while performing the operations
Давайте попробуем разобраться, что не так с выводами и кодом автора.
С моей точки зрения проблема в том что автор статьи не умеет пользоваться Stream’ами и это проблема с которой приходиться довольно часто сталкиваться. У этого явления есть, на мой взгляд, три причины:
- Сложная история Node.js Stream API – боль и страдания описаны тут
- Не самое интуитивное API, если пытаться пользоваться ним без каких-либо оберток
- Довольно странная документация, которая представляет Stream’ы как что-то очень сложное и низкоуровневое
Все вместе это приводит к тому, что разработчики довольно часто не умеют и не хотят использовать Stream API.
Что не так с кодом автора?
Для начала повторим тут задачу(оригинал на английском и ссылку на файл можно найти в посте):
Есть некий файл размером 2.5 ГБ со строками вида:
C00084871|N|M3|P|201703099050762757|15|IND|COLLINS, DARREN ROBERT|SOUTHLAKE|TX|760928782|CELANESE|VPCHOP&TECH|02282017|153||PR2552193345215|1151824||P/R DEDUCTION ($76.92 BI-WEEKLY)|4030920171380058715
Его нужно распарсить и узнать следующую информацию:
- Количество строк в файле
- Имена на 432-ой и 43243-ей строках(тут правда возникает вопрос как считать, с 0 или 1?)
- Самое часто встречаемое имя и сколько раз оно встречается
- Количество взносов по по каждому месяцу
В чем проблема? – Автор честно говорит, что загружает весь файл в память и из-за этого Node “вешается” и автор приводит нам интересный факт.
Fun fact: Node.js can only hold up to 1.67GB in memory at any one time
Автор делает из этого факта странный вывод, что это Stream’ы загружают весь файл в память, а не он написал неправильный код.
Давайте опровергнем тезис: "Although Node.js was streaming the inputs and outputs, it still attempted to hold the whole file", написав небольшую программу, которая посчитает количество строк в файле любого размера:
const { Writable } = require('stream')
const fs = require('fs')
const split = require('split')
let counter = 0
const linecounter = new Writable({
write(chunk, encoding, callback) {
counter = counter + 1
callback()
},
writev(chunks, callback) {
counter = counter + chunks.length
callback()
}
})
fs.createReadStream('itcont.txt')
.pipe(split())
.pipe(linecounter)
linecounter.on('finish', function() {
console.log(counter)
})
N.B.: код намерено написан максимально просто. Глобальные переменные это плохо!
На что стоит обратить внимания:
- split – npm пакет который на “вход” принимает поток строк – на “выход” отдает поток наборов строк разделенным переносом строки. Скорее всего сделан как реализация Transformation stream. Мы в него pipe’ем наш ReadStream с файлом, а его самого pipe’ем в...
- linecounter — имплементация WritableStream. В ней мы реализуем два метода: для обработки одного кусочка(chunk) и нескольких. “Кусочком” в этой ситуации выступает линия кода. Обратка – добавление к счетчику нужного числа. Важно понимать – мы не будем загружать в этой ситуации весь файл в память, а API за нас поделит все на максимально удобные для обработки “кусочки”
- ‘finish’ – события которое “произойдет” когда “закончатся” данные поступающие на наш ReadableStream. Когда это произойдет мы залогируем данные счетчика
Ну что ж, испытаем наше творение на большом файле:
> node linecounter.js
13903993
Как видим – все работает. Из чего можем сделать вывод что Stream API прекрасно справляется с файлами любого размера и утверждение автора поста, мягко говоря, не верно. Приблизительно также мы можем посчитать любое другое значение требуемое в задаче.
Расскажите:
- Интересно ли вам почитать как решить задачу полностью и как привести получившийся код в удобный для сопровождения вид?
- Используете ли вы Stream API и с какими трудностями вы сталкивались?