В интернете опять кто-то не прав – во вчерашнем 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’ами и это проблема с которой приходиться довольно часто сталкиваться. У этого явления есть, на мой взгляд, три причины:


  1. Сложная история Node.js Stream API – боль и страдания описаны тут
  2. Не самое интуитивное API, если пытаться пользоваться ним без каких-либо оберток
  3. Довольно странная документация, которая представляет 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 и с какими трудностями вы сталкивались?

Комментарии (10)


  1. little-brother
    26.10.2018 21:13
    +2

    Ожидал от названия статьи нечто большего.
    В туторах Кантора (25, 26) описывается и эта проблема и ряд других, менее очевидных.


    1. HaMI Автор
      26.10.2018 22:31

      расскажите, о чем бы вам хотелось узнать?


      1. little-brother
        26.10.2018 23:28

        Ни о чем конкретно. Зашел, думал узнать что-то новое, но нет.


    1. HaMI Автор
      27.10.2018 16:19

      могу предложить эту статью habr.com/post/325320
      возможно вас заинтересует


  1. surefire
    26.10.2018 22:00
    +1

    Для этой задачи вообще никакие пакеты из NPM не нужны и велосипеды тоже.
    Все что нужно уже есть из коробки, даже с примерами.
    https://nodejs.org/api/readline.html#readline_example_read_file_stream_line_by_line


    1. vmb
      27.10.2018 23:21
      +2

      А скоро станет ещё проще: readline: add support for async iteration


  1. worldxaker
    26.10.2018 23:25

    ну и на память на самом деле ограничения нет. достаточно задать флаг max_old_space_size


  1. funca
    26.10.2018 23:54

    Streams одно из фундаментальных и костыльных решений в node.js. Самое крутое что там есть это backpressure. Самое отстойное — обработка ошибок. Нормально сделали в https://highlandjs.org. Но сейчас есть и более удобные штуки типа ES6 генераторов для pull и Observable для push подходов.


    1. HaMI Автор
      27.10.2018 16:16

      когда вы говорите Observable – вы Rx подразумеваетe?


  1. HaMI Автор
    27.10.2018 16:15

    когда вы говорите Observable – вы Rx подразумевает?