Pull to refresh

Comments 57

1) Мне думается, что вы здесь намеряли не скорость чтения файла, а скорость взятия файла из кэша. Нужно читать разные файлы, а не один и тот же
2) Какой смысл теста без указания ОС, типа носителя и файловой системы? Там тоже результат может отличаться в разы!
Спасибо за комментарий, исправил.
Отлично! и выводы из статьи стали гораздо точнее!

Почему Multithread loop readFileSync дает такой низкий результат? Это очень странно. Возможно, тесту для исполнения попросту не хватало памяти? Или антивирус шалил?


Надо было попробовать создавать процессы заранее, и через fork вместо execFile, а в тесте выдавать процессам задания.

Бенчмарк исправил, результаты добавил.

Вроде как суть в том, что при readFile мы можем запустить другие операции (реагировать на http-запросы, посылать запрос в БД или там считать факториал), а при readFileAsync — нет.

Данный бенчмарк, как верно заметили выше, измеряет скорость взятия одного файла с одного диска.


Преимущество асинхронных IO запросов раскроется только если одновременно читать/писать файлы с разных дисков, разных сетевых подключений и так далее.

На самом деле все еще хуже. Нет смысла читать один и тот же файл с диска много раз: надо прочесть его 1 раз и использовать. Это будет еще быстрее.


Если же файл нельзя прочитать и запомнить — значит, он слишком большой. Но тогда и читать его в память целиком тоже нельзя — нужно использовать потоковое чтение. А у него синхронной версии просто нет.

Что-то мне подсказывает, что для реально больших (100мб и больше) файлов ещё сильно взыграет hdd/ssd эффект — грубо говоря multithread read на hdd должен загнуться по отношению к single thread, а на ssd — наоборот, вырасти слегка.

multithread read на hdd тоже может вырасти от одновременности, благодаря появлению у дискового драйвера возможности выбирать какой блок читать первее. Но это только при условии достаточного размера файловых буферов.

Ну и поделюсь своей болью:


Клиент запрашивает 4 файла. Генерация каждого из них занимает секунду. Из-за асинхронной выдачи файла получается, что пока не сгенерятся все 4 — клиент не сможет загрузить даже первый сгенеренный.


Как это выглядело бы в синхронном виде: сгенерили первый файл за секунду — отдали, второй ещё через секунду и тд. Но такой опции в ноде нет. :-(

попробуйте так:


app.get('/files', async (req, res) => {
   for(const file of files) {
      const content = await generateFile(file);
      res.write(content);
   }
   res.end(200);
});

По идее, клиент будет видеть контент по мере готовности, если я правильно понял задачу.

Не, тут проблема в том, что первый файл уже начался отдаваться, но не может закончиться, так как сервер тут же берётся за генерацию следующего файла. Ну и никакого списка файлов нет — клиент их запрашивает отдельными запросами.

а почему первый файл не может до конца отдаться?
файлы генерируются синхронно и блокируют ввод/вывод?

Да, генерятся синхронно, а отдаются асинхронно по частям.

самый очевидный ответ: генерируйте в отдельном процессе, не блокируя основной.
Или с этим тоже не так просто?

О том и речь, что нужно креативить поднятие генератора в отдельном процессе и гонять между ними данные. А это уже ничем не отличается от того же синхронного php спрятанного за веб-сервером.

Если у вас задача "сделать не как у php", то это сложно, да.
А в целом, будет же нормально работать. Возможно понадобится использовать что-то типа worker pool, чтобы процессов было не слишком много.

Моя задача сделать хорошо и в концепции ноды это не так просто. Мне ещё и кешировать надо промежуточные результаты, так что в воркер нужно выносить фактически всё приложение, оставляя в основном процессе голый экспресс.

Исходная задача(синхронные операции длительностью в 1 секунду) — уже не в концепции ноды
Это решается либо переносом в воркер, либо превращением синхронного кода в асинхронный.

В последнем случае, начиная с корневой функции вашего генератора превращаете ее в async-функцию, сдабривая await-ами nextTick-а и await-ами дочерних вызовов
Затем делаете тоже самое с дочерними функциями
Продолжаем до тех пор пока синхронный код между двумя await-ами не будет занимать приемлемое время(например меньше 50мс)
В итоге во время генерации нового файла предыдущие файлы прекрасно могут отдаваться
Более того, если генерацию запросили одновременно два клиента, то второй не будет ждать первого, а их файлы будут генериться параллельно(но дольше). Если нужно это предотвратить — оберните внешнюю функцию в однопоточный семафор

Боюсь я не готов ещё перелопачивать несколько мегабайт исходников компилятора тайпскрипта :-)

А он-то тут каким боком? Вы что, на проде компилируете скрипты на лету?

То, что для одного "прод", для другого — "окружение разработчика" :-)

UFO just landed and posted this here

Рабочие процессы кластера уже заняты express — так что пул воркеров придется самому писать.

А может все-таки лучше попробовать Webpack и его Dev-server?
Будет вам и пересборка на лету и кеширование не изменявшихся файлов

Он разве умеет собирать бандлы исходя из урла (а не только те, для которых прописаны конфиги), автоматически детектировать зависимости (а не писать портянки импортов), подключать директории целиком (а не каждый файл по отдельности), выдавать всегда актуальный результат (а не результат последней пересборки в n процентах случаев), адекватно разрешать циклические зависимости (с учётом их приоритета)?

Ядро вебпака умеет только файлики клеить по импортам. Все ваши хотелки могут быть плагинами:


собирать бандлы исходя из урла

вот тут не очень понятно. Как он будет собирать то, для чего конфиги не написаны? Нужен более конкретный пример


автоматически детектировать зависимости

напишите свой лоадер, вызывайте в нем addDependency по своим правилам


подключать директории целиком

require.context()


выдавать всегда актуальный результат

webpack-dev-server придерживает запрос и резолвит его только по окончании пересборки.


адекватно разрешать циклические зависимости

видимо это к автоматической детекции зависимостей. Напишите свой лоадер, обрабатывайте там приоритеты по своим правилам.

Как он будет собирать то, для чего конфиги не написаны? Нужен более конкретный пример

Запросил клиент /mol/app/files/-/web.js — собрался бандл, включающий как собственно содержимое /mol/app/files, так и всех необходимых этому приложению зависимостей. Всего приложений в одной кодовой базе десятки, а модулей, которые можно скомпилировать в независимые библиотеки — сотни. Аналогично и с web.test.js, web.view.tree, web.css и ку чей других типов бандлов.


напишите свой лоадер, вызывайте в нем addDependency по своим правилам

Ему можно скормить директорию, чтобы он сам подключил все файлы из неё в правильном порядке?


require.context()

Речь про директории как зависимости, а не про резолв путей.


webpack-dev-server придерживает запрос и резолвит его только по окончании пересборки.

И тут мы возвращаемся к началу ветки. Если нужно сгенерить 4 файла, то первый будет отдан, когда будут все 4 файла сгенерены или они реализовали вынос TS компилятора в отдельный воркер?


видимо это к автоматической детекции зависимостей.

Нет, это к разрешению циклических зависимостей.


class Logger extends Object {}

class Object {
    logger() {
        return new Logger
    }
}

Напишите свой лоадер, обрабатывайте там приоритеты по своим правилам.

Столько своего надо написать… Вот и получается, что от вебпака остаётся лишь:


только файлики клеить
Запросил клиент /mol/app/files/-/web.js — собрался бандл, включающий как собственно содержимое /mol/app/files

Такой фичи вроде там нет. И скорее всего потому, что большинству пользователей и без нее хорошо. Если очень нужно, можете запилить свой dev-сервер на основе существующей миддлвары


Ему можно скормить директорию, чтобы он сам подключил все файлы из неё в правильном порядке?

Ваш лоадер, ваши правила: fs.readdir().forEach(() => this.addDependency()). Будет как вам надо


И тут мы возвращаемся к началу ветки. Если нужно сгенерить 4 файла, то первый будет отдан

Все эти четыре файла нужны будут на одной странице. Логично, что можно подождать их все, раз без них не будет ничего работать.


Разделить сборку разных страниц по отдельности можно. webpack([configForPage1, configForPage2, ...]). Вебпак будет собирать несколько бандлов параллельно, загрузку друг друга они не блокируют


Нет, это к разрешению циклических зависимостей.

В спецификации CommonJS определено, что делать в этом случае. Webpack ей следует. Ваш пример будет работать как надо.


Столько своего надо написать… Вот и получается, что от вебпака остаётся лишь:

А также вам дается готовая экосистема уже готовых плагинов. Autoprefixer, инлайн картинок, babel/typerscript и другие ништяки подключаются в пару строк.

И скорее всего потому, что большинству пользователей и без нее хорошо.

Стокгольмский синдром.


Если очень нужно, можете запилить свой dev-сервер на основе существующей миддлвары

А чем express не угодил?


Ваш лоадер, ваши правила: fs.readdir().forEach(() => this.addDependency()). Будет как вам надо

Я пробовал реализовывать похожую схему. У неё 2 недостатка: медленно при большом числе файлов и в банд файлы из одного пакета попадают не рядом а вразнобой.


Все эти четыре файла нужны будут на одной странице. Логично, что можно подождать их все, раз без них не будет ничего работать.

2 бандла необходимы сразу, ещё 3 подгружаются уже после. Генерация web.js — 6 секунд, генерация web.test.js — ещё 6. Появление страницы через 6 секунд лучше, чем через 12.


В спецификации CommonJS определено, что делать в этом случае. Webpack ей следует.

CommonJS предполагает явное объявление зависимостей, а у нас они трекаются автоматически.


А также вам дается готовая экосистема уже готовых плагинов.

Раздутая экосистема, настройка которой сравнима с написанием того же руками.


Autoprefixer

postCSS у нас и так подключен.


инлайн картинок

нафиг надо


babel/typerscript

TS у нас тоже работает.


и другие ништяки

Какие?

Насколько я знаю, webpack собирает все зависимости в одном процессе. И если генерация файла останавливает цикл обработки сообщений на секунду — то и в дев-сервере вебпака будет происходить то же самое.

Подобные оптимизации мне кажутся экономией на спичках. Если у вас такая большая кодовая база, что пересборка уже запущенного проект занимает ощутимое время, то вы явно делаете что-то не то.


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

Причем тут "разгон сборки"? Проблема же не в медленной сборке, а в том что это получилась stop-the-world сборка.

О каком world вы говорите? Dev-сервер запускается локально и отдает файлы только вам, никого другого он не блокирует.

Читайте внимательнее:


То, что для одного "прод", для другого — "окружение разработчика" :-)

Я, правда, так и не понял это это означает — то ли кто-то на проде разрабатывает, то ли кто-то на чужой дев-сервер ходит...

В документации webpack-dev-server прямым текстом написано


The tools in this guide are only meant for development, please avoid using them in production!!

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

Для разработчика инструмента "прод" — машина разработчика приложений.

У вас разработчик инструмента ковыряется прямо в машине разработчика приложений?


Или разработчик приложений зачем-то каждый раз пересобирает свои инструменты?

Разработчик приложения пересобирает приложение моим инструментом. Что тут не понятно?

В таком случае при чем тут прод? Обычная ситуация с дев-сервером.


Чем же в таком случае вас не устраивает стандартный подход — фоновая пересборка проекта при изменениях?


Которая запускается не в момент запроса страницы, а в момент изменения исходника?

В таком случае при чем тут прод?

А что такое "прод" по вашему?


Чем же в таком случае вас не устраивает стандартный подход — фоновая пересборка проекта при изменениях?

https://github.com/eigenmethod/mol/issues/254

TS не быстро компилирует и ему нужны все файлы, чтобы протрекать все типы. У меня есть мысль сделать пофайловую компиляцию с подсовыванием нагенеренных d.ts от зависимостей. Тогда горячая пересборка думаю будет почти мгновенной.

UFO just landed and posted this here
UFO just landed and posted this here
Можно попробовать следующий подход:
— быстро сгенерировать список уникальных имен для файлов и отдать клиенту
— параллельно запустить процесс генерации файлов

Клиент получает список file_ids – и запрашивает их на скачивание, получает каждый по готовности.

ps: в целом, это даже не проблема ноды, это довольно распространенная задача при генерации контента.

На Go, D, PHP эта задача решается тривиально, и только в ноде нужно плясать с отдельными процессами. Но нет, это не проблема ноды :-)

Возможно я не до конца понимаю проблему, но в php это было бы примерно так же как я описал. Или основной консерн – отдельные инстансы ноды или кластер?

ps: да и вообще, нода не предназначена для генерации / парсинга и любых других CPU-bound задач. Ее удел – взять темплейт, подставить значения и отдать наружу. Или сходить на другие сервисы, получить контент и связать его воедино.

К сожалению TS компилятор только под ноду завезли :(

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


Так что если в общем процессе компилятор ведет себя как-то неправильно — всегда есть возможность откатиться на unix-way, и недостатком ноды такое не является.

Конечно же нода идеальна :-)

Если я правильно понимаю, это работать не будет по двум причинам:
1. res.end(200) вы отдаете после того, как сгенерировали все 4 файла, а это уже через 4 сек. и если это так – ответ не уйдет принимающей стороне все это время.
2. разве мульти-парт контент может быть отдан для скачивания?

Ну да, немного напутал в правильном вызове методов. Вот работающее демо:


Код примера
const http = require('http');
const {promisify} = require('util');

const delayed = promisify(setTimeout);

const files = ['first', 'second', 'third', 'forth'];

http.createServer(async (request, response) => {
    response.setHeader('Content-Type', 'text/html; charset=UTF-8');
    response.setHeader('Transfer-Encoding', 'chunked');

    response.write('<h1>Chunked transfer encoding test</h1><ul>');
    for(const file of files) {
        await delayed(2000);
        response.write(`<li>${file}</li>`);
    }
    response.end('</ul><h5>Done</h5>');
}).listen(8080);

Запускаете, открываете http://localhost:8080 и видите, как пункты меню появляются по мере готовности.

Sign up to leave a comment.

Articles