SuperOleg dev notes
1.82K subscribers
56 photos
148 links
Обзоры новостей и статей из мира frontend, интересные кейсы, исследования и мысли вслух

https://github.com/SuperOleg39

https://twitter.com/ODrapeza

@SuperOleg39
Download Telegram
Привет!

Попалась очень интересная статья от Gitpod, про их инфраструктуру для development окружений, и как они ушли от Kubernetes к кастомному решению.

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

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

Но всегда были интересно как устроены песочницы, такие как Codesandbox и Stackblitz, в блоге которых тоже попадаются классные статьи, иногда про них пишу - https://t.me/super_oleg_dev/141, и решаемые проблемы в статье Gitpod во многом с ними пересекаются.

Итак, Gitpod это классное облачное решение для разработки.

Такое development окружение имеет ряд особенностей:
- наличие состояния, которое так просто с одной ноды на другую не перенести - исходники, собранный код, кэши и так далее
- вообще не вариант терять изменения в исходном коде при разработке
- непредсказуемое потребление ресурсов, например пиковые потребления на сборку, и минимальные в остальное время
- безопасность, зачастую разработчикам нужен root доступ на ноде (вплоть до возможности развернуть свой Docker и k8s, да, внутри докера и k8s :crazy: ), это не должно аффектить другие нодыв кластере

Kubernetes был выбран для инфраструктуры Gitpod как очевидное решение, но столкнулись с рядом сложностей, и трудности в управлении и поддержки при масштабировании, и то что k8s заточен под контролируемые окружения и нагрузки, далее подробно разбираются отдельные кейсы.

Первая проблема - управление ресурсами, а именно CPU, память и сеть (пропускная способность).

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

Пробовали схемы с Completely Fair Scheduler (CFS), но он не предсказывает потребление, а увеличивает ресурсы когда их не хватает - то есть уже слишком поздно.

Если выделять статичные ресурсы, тут тоже проблема, разные процессы (даже VS Code запустит пачку процессов) в итоге могут конкурировать и CPU так же будет не хватать.

Пробовали приоритезацию процессов, но и там свои трудности, связанные с реализацией механизма.

В итоге все замиксовали и остановились на решении с динамическим выделением ресурсов (появилось в k8s) + CFS + приоритеты процессов основанные на cgroupsv2.
Следующая проблема - управление памятью.

Просто так выделить фиксированный X gb памяти на ноду - не вариант, оперативка в облаке это один из самых дорогих ресурсов.

Только появление swap-space в k8s позволило решить проблему перенаправления памяти между подами, потому что ранее это означало необходимость прибивать процессы.

В целом, поддержка memory swap практически убрала необходимость в перенаправлении выделенной памяти. Swap (или файл подкачки) - позволяет не активные данные сбросить из оперативки на диск, и обратно.

Далее, проблема - оптимизация производительности хранилища (I/O операции)

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

Тут рассказывают про баланс между стоимость, скоростью и надежностью и какие решения смотрели (из всего знаком только с s3):
- SSD RAID 0 - очень быстро, но привязано к конкретной ноде, выйдет из строя конкретный диск - все данные потеряны. Этот подход используют на данный момент, таких инциндентов не было.
- Block Storage - виды хранилищ который привязаны к нодам, те же проблемы с потерей данных, медленнее, но широко распространены
- Persistent Volume Claims - k8s абстракция поверх реального хранилища в кластере, claim - по сути запрос на конкретный размер диска, нужные права и прочее. Гибко, но есть проблемы со временем привязки на старте, надежностью, и другие ограничения.

Бэкапы и восстановление дисков очень затратная операция, тут выбрано интересное решение:
- архивы заливаются в s3 и скачиваются оттуда
- архивы не сжатые
- дело в том что обычно упираются в CPU, а не в пропускную способность сети, и не выгодно сжимать и распаковывать эти архивы (но тут подчеркивают что важен баланс)

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

Используют похоже какой-то кастомный механизм - лимитер на основе cgroups2, нагуглил пример - https://andrestc.com/post/cgroups-io/
Интересный кейс от коллеги из моей команды.

Кстати так и не разобрались с разными хэшами при сборке одного и того же кода, просто Terser рандомно при минификации меняет местами две однобуквенные переменные.

Рекомендую подписаться на канал Лёши, рассказывает про интересные кейсы из совершенно разных сфер нашей разработки.

https://t.me/royal_blue_dev
Forwarded from royal_blue_dev
Методология The Twelve-Factor App требует строгого разделения сборки и рантайма. Технически это значит что docker-образ приложения привязывается к хэшу коммита, на котором собирается. Главное – это позволяет релизить в прод именно тот код, который был протестирован на стенде. Такой подход дает определенные гарантии, однако, порождает и непростые ситуации:
• Продукт одной из команд – это NodeJS SSR приложение. Важная часть таких приложений, это stats.json файл, который содержит информацию о получившихся при сборке файлах. Сами файлы раздаются с CDN, соответственно и загрузка их туда является шагом сборки;
• Поскольку stats.json зашивается внутрь docker-образа и переиспользуется между деплоями с одного коммита, мы точно также должны переиспользовать файлы, загружаемые на CDN, поэтому на этапе сборки пакуем получившиеся файлы в .zip-архив и сохраняем на S3;
• Ссылки на файлы содержат хэши, которые должны быть идемпотентными, но это не всегда так, например, из-за багов в инструментах сборки. Таким образом, мы получаем критическую неочевидную связь образ => архив, так как для образа и архива хэши файлов должны совпадать. Несовпадение хэшей – риск полной недоступности приложения, поскольку приложение будет запрашивать файлы которых может не быть на CDN;
• Образы хранятся в Artifactory, который имеет политики очистки. В нашем случае, для нерелизных образов это 7 дней. Отсюда следует, что если кто-то создаст МР, задеплоит его, подождет 7 дней, а потом задеплоит приложение еще раз или откатится на коммит, для которого нет образа в Artifactory и при этом разойдутся хэши файлов – приложение не будет работать. Это не очень страшно при деплое, но катастрофа при откате: выпустили приложение => решили откатить => откатили и приложение все равно не работает;
• Вишенка на торте: у S3 тоже есть политики очистки, и здесь все работает с точностью да наоборот;

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

Не призываю отказываться от The Twelve-Factor App, считаю ее очень важной и оберегающей от множества ошибок, скорее хочу подсветить, что при разделении системы на части автоматически вылезают проблемы синхронизации состояния. Стоит учитывать это при проектировании.
Привет!

По возможности продолжаю обзор статьи - https://www.gitpod.io/blog/we-are-leaving-kubernetes

Итак, мы узнали про разные подходы к управлением CPU, памятью и дисковым пространством.

Следующая часть статьи - про оптимизацию старта и автоскейл.

Быстрый старт окружений критично важен для такого сервиса как Gitpod, но это требование конфликтует с желанием максимально утилизировать ресурсы.

Первое что не сработало - это попытка запускать множество воркспейсов на одной ноде, рассчитывали что общие кэши (возможно это про опцию host IPC - https://www.fairwinds.com/blog/kubernetes-basics-tutorial-host-ipc-should-not-be-configured) ускорят старт. Но оказалось что в k8s все равно есть накладные расходы на перемещение контента в нужное место.

Далее провели ряд экспериментов:

Ghost workspaces - с помощью кастомного планировщика задач создавали поды - пустышки, которые занимали место для возможного автоскейла и замены на настоящие поды. Оказалось медленно и ненадежно.

Ballast pods - эволюция предыдущего подхода, поды - призраки заполняли ноду целиком, что ускорило и удешевило замену.

Затем в k8s появился плагин автоскейлер который позволил убрать костыли и отлично решает проблему - https://kubernetes.io/docs/concepts/cluster-administration/cluster-autoscaling/

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

Pause image - это контейнер который хранит (занимает?) network namespace для пода, все прочие контейнеры созданные в поде будут использовать именно его, ссылки по теме:
- https://www.ianlewis.org/en/almighty-pause-container
- https://blog.scottlowe.org/2013/09/04/introducing-linux-network-namespaces/
Следующая часть - оптимизация процесса скачки (pull) Docker образов.

Образы воркспейсов могут занимать до 10гб (включает все тулзы доступные для разработчика), скачивать такое долго и дорого, опять таки сильно влияет на скорость старта воркспейса.

Я кстати подзапутался с терминами, но похоже так - воркспейс это чисто термин Gitpod, а прочие (нода/под/контейнер) уже термины k8s - https://bool.dev/blog/detail/chto-takoe-pods-nodes-containers-i-clusters-v-kubernetes

Тут много интересных оптимизаций:

Предзагрузка образов с помощью DaemonSet - https://aws.amazon.com/blogs/containers/start-pods-faster-by-prefetching-images/.
Плохо показывает себя при масштабировании так как при старте новых нод образ там еще не присутствует. Плюс предзагрузка теперь конкурирует за сеть и CPU с процессом старта воркспейса.

Максимальное переиспользование image layers - https://docs.docker.com/get-started/docker-concepts/building-images/understanding-image-layers/.
Даже создали тулзу для сборки образов - https://github.com/gitpod-io/dazzle.
Но поняли что не могут нормально замерять эффективность переиспользования. В причинах указано что-то про Open Container Initiative (https://github.com/opencontainers/image-spec/blob/main/spec.md), не знаком с термином, поэтому не разобрался в чем именно причина.

В любом случае мне кажется что для своих конкретных проектов, где есть проблема с размером образа и скоростью его сборки и загрузки, слои это перспективная оптимизация.
Еще ссылка по теме - https://www.ctl.io/developers/blog/post/caching-docker-images

Далее - предварительно запекание образов (сразу приходит в голову аналогия с запеканием теней и текстур в геймдеве). Образ воркспейса заранее сохраняли в образе ноды (тут должен быть мем pimp my ride). Ускоряет старт, но образы очень быстро устаревают. Также не работает для self-hosted использования.

Попробовали некий Stargazer и ленивую загрузку образов, похоже это про https://medium.com/nttlabs/startup-containers-in-lightning-speed-with-lazy-image-distribution-on-containerd-243d94522361.
Но это требует отдельную обработку каждого образа, плюс не все реестры контейнеров поддерживают.

Интересная кстати вещь этот Stargz Snapshotter. Суть ленивой загрузки образов - контейнер может быть запущен не дожидаясь завершения загрузки! А все необходимые части образа будут дозагружены по требованию (чисто Streaming Rendering в devops мире).
Графики в README показывают мощные результаты - https://github.com/containerd/stargz-snapshotter

Последнее это Registry-facade + IPFS. Сразу два незнакомых мне термина:
- https://github.com/httptoolkit/docker-registry-facade
- https://habr.com/ru/articles/314768/ (на секундочку межпланетная сеть)
Круто работает но сильно усложняет систему. У них есть про это отдельный доклад - https://www.youtube.com/watch?v=kS6aDScfVuw

В итоге нет одной идеальной оптимизации и все по сути набор компромиссов.
Представьте себе мир, в котором вы снова можете легко контрибьютить в JS тулинг 😊

Порадовал порт TS на Go, как человека которому Rust не даётся (и в целом похожи на хайптрейн тренды переписывать весь тулинг на него).

Хорошее объяснение почему Go - https://www.reddit.com/r/javascript/s/nUMcx2sIWt

Тут можно вспомнить попытку написать порт на Go автора SWC по таким же причинам - https://kdy1.dev/2022-1-26-porting-tsc-to-go
Ещё из интересного, недавно увидел что ведутся работы по поддержке Rspack в Next.js - https://github.com/vercel/next.js/blob/canary/packages%2Fnext-plugin-rspack%2FREADME.md

С одной стороны прямая конкуренция Turbopack, с другой - дешёвая возможность сильно улучшить жизнь пользователям с Webpack.

Мы в свою очередь потихоньку рефакторим @tramvai/cli сразу для двух оптимизаций:
- вынести сборщики в отдельные плагины, что позволит легко сделать новый Rspack плагин
- разнести по разным воркерам текущую Webpack сборку (которая будет жить ещё долго), так как multi compiler сборка вебпака для серверного и клиентского кода сильно конкурирует между собой в одном Node.js процессе
Привет!

Одной из недавних больших задач была интеграция Opentelemetry в Tramvai - https://tramvai.dev/docs/features/monitoring/telemetry

Экосистема вокруг Opentelemetry очень развитая, крутая документация, есть SDK под разные ЯП, для Node.js множество интеграций под всевозможные фреймворки и встроенные модули.

Основной рекомендуемый подход к интеграции телеметрии - подключить авторинструментарий, который все сделает за нас, и настроить экспорт трейсов в коллектор.

Меня в этой экосистеме смутили несколько вещей:

Повсеместное использование синглтонов (не самый лучший паттерн для Node.js по ряду причин) - общие сущности составляющие ядро телеметрии (Trace, Context и тд) - создаются под капотом как глобальные переменные, хотя Async Local Storage позволяет легко этого избежать (и он уже используется для контекста).

С этим пришлось смириться, даже создавая свой экземпляр трейсера, ряд кейсов по другому не решить, надо использовать синглтоны trace / context.

Автоинструментарий особенно интересен:
- тянет несколько десятков модулей, в рантайме определяется какой из них использовать если стоит соответствующая либа и так далее (это конфигурируется)
- большая часть автоинструментария работает по принципу манкипатчинга - за исключением к примеру Fastify где есть конкретный механизм расширения, хуки
- множество бесполезных спанов - если брать Fastify, каждый хук оборачивается в спаны, зачем нам вся эта информация в трейсах?

Манкипатчинг в Node.js экосистеме для встроенных модулей - к сожалению наша реальность, хрупкая но единственная возможность мониторить к примеру исходящие запросы, dns lookups и так далее.

Тут отойду в сторону и отмечу что крутая инициатива Deno по встроенной телеметрии - https://docs.deno.com/runtime/fundamentals/open_telemetry/

А также интересная архитектура в Undici которая расширяемая из коробки в отличие от http.request - https://blog.platformatic.dev/http-fundamentals-understanding-undici-and-its-working-mechanism

И автоинструментарий для Undici к примеру без манкипатчинга а слушает конкретные события, ссылка на исходники. Хотя тут интересно что события по сути отладочные, используется diagnostic_channel.
Манкипатчинг еще ведет к такой особенности, что подключать автоинструментарий надо раньше чем весь остальной код приложения.

Рекомендуемый подход - создать отдельный файл с интеграцией автоинструментария, запустить его до сервера:

node --require ./instrumentation.js app.js


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

В итоге я пришел к нескольких принципам при создании интеграции:
- хочу отдельный модуль, который работает при подключении без дополнительных действий
- модуль не использует автоинструментарий, самостоятельно покрываю базовый функционал телеметрией по Semantic Conventions
- делаю удобный трейсер, его и всю конфигурацию храню в DI контейнере

Сделать кастомный Tracer оказалось не сложно, есть отличный пример у Next.js которые в свою очередь подсмотрели у Datadog:
- https://github.com/DataDog/dd-trace-js/blob/59e9a2a75f4256755b4e6c9951a0bdf8d39b4015/index.d.ts#L9
- https://github.com/vercel/next.js/blob/9a1cd356dbafbfcf23d1b9ec05f772f766d05580/packages/next/src/server/lib/trace/tracer.ts#L74

API трейсера получилось такого вида, и инкапсулирует очень бойлерплейтный код Opentelemetry:

tracer.trace('trace-name', { ...spanParameters }, async (span) => { ... });


Ключевой момент, что любую операцию надо обернуть в этот асинхронный коллбэк, для поддержки вложенного контекста (который как раз реализован через Async Local Storage), что бы каждый новый span получал родительский спан из контекста выше, и они автоматически будут выстроены в цепочку при анализе трейсов:

root span
- child span
- nested child span
Необходимость оборачивание операций, которые мы хотим мониторить, в спаны - приводит к новому челленджу.

Допустим в Fastify есть хук onRequest, который в коллбэке передает нам метод done - он содержит весь стек вызовов на обработку текущего запроса, и весь запрос легко обернуть в спан:

app.addHook('onRequest', (req, reply, done) => {
tracer.trace('GET', async () => {
done();
})
})


Но в Tramvai есть базовые механизмы, которые мы тоже хотим мониторить:
- линии Command Line Runner
- значимые хуки роутера
- запросы через наши Http Client

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

Я рассматривал два основных варианта как референс - хуки Fastify, и хуки Webpack Tapable, на последнем варианте в итоге и остановился.

Реализовал несколько кастомных Tapable хуков, которые позволяют добавить в цепочку или для параллельного запуска любые действия (синхронные и асинхронные), а также обернуть их, самый важный для нас момент.

Примеры Tapable хуков можно посмотреть в юнит-тесте - https://github.com/tramvaijs/tramvai/blob/main/packages/libs/hooks/src/tapable.spec.ts

Отрефакторил Command Line Runner, Router, Http Client interceptors, тут сразу подсвечу мою недоработку которая может в будущем выстрелить - стоило сделать подмену реализаций (выбор старой или новой реализации) через параметры сборки, я же просто создал копии файлов, отрефакторил и переключил старый на новый хардкодом, прогнав все тесты.

Это позволило сделать простой кастомный автоинструментарий, состоящий из оборачивания подходящих Tapable хуков, пример с роутингом - https://github.com/tramvaijs/tramvai/blob/f9d7bde4991b07689b95347efd64b9eea17b59b8/packages/modules/opentelemetry/src/instrumentation/router.ts#L34

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

И конечно тут не без значимых минусов:
- хуки и особенно оборачивание (через метод wrap) раздувает стек вызовов JS кода, заметно усложняет отладку
- также это незначительно, но влияет на перформанс, но тут заметнее влияние на создание большого количества спанов (повод не делать их слишком много и не мониторить мелкие задачи)
- хуки усложняют код, делают поведение не линейным и менее прозрачным - сделал для всех хуков описание и визуализацию - https://tramvai.dev/docs/features/app-lifecycle#commandlinerunner-hooks

В итоге уже время и практика использования покажет, насколько было верное решение)

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

Постарался вернуть как было, а если будет проблема со спамом, спрошу вашего совета как решаете в своих каналах)
Привет!

Как-то писал про серверные оптимизации ноды, и в частности про параметр --max-semi-space-size - https://t.me/super_oleg_dev/115

Часто появляются новые кейсы которые показывают сколько же нюансов в работе GC ноды.

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

Оказалось, что в условиях ограниченных k8s лимитов, у этого приложения дефолтные значения --max-old-space-size были выше или примерно равны лимиту по памяти для подов этого приложения.

Чем больше параметр, тем меньше времени GC тратит на очистку неиспользуемой памяти при ее низком потреблении, что при выделенных 512mb памяти на под, и max-old-space-size размером в 1gb, позволит накопиться мусору и приведет к OOM.

Также, этот параметр не ограничивает все потребление памяти Node.js процессом, а только ее часть, то есть даже при выделенных 1gb памяти на под, значение max-old-space-size должно быть ниже 1gb, иначе приведет к OOM.

Хорошо написано про параметр в этом документе, предлагают ставить max-old-space-size примерно в 75% от доступной памяти - https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/docker/memory-limit.md

Сегодня разбирался с обратной проблемой.

У приложения большое потребление памяти, долго работает GC, высокий лаг event loop.

Обнаружил что у коллег все стало сильно хуже когда подняли --max-old-space-size с 2gb до 4gb.

Погонял приложение локально, даже с кэшами ему нужно памяти сотни мегабайт, но никак не гигабайты. Правда важно учесть нюанс, что SSR запросы на больших страницах дорогие и по памяти, это могут быть мегабайты строк (HTML, стейт и прочее) в памяти на каждый запрос и конечно тут нужно иметь запас на случай нагрузок.

Еще раз погонял нагрузки на приложении уже с увеличенным --max-old-space-size, и увидел что сильно поднимается пиковое потребление памяти, 1-2 гигабайта легко, которые потом очищаются до пары сотен мегабайт.

Как это работает:
- приложению выделено слишком много памяти
- GC работает реже
- потребление памяти быстро повышается (на продакшене постоянные стабильные нагрузки)
- и тут GC начинает медленно работать, особенно заметно на major очистках!

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

Теперь я хочу исследовать поподробнее, хорошая ли наша базовая рекомендация повышать дефолты --max-semi-space-size, так как этот параметр ведет к еще более заметному накоплению памяти между очистками, и будет ли корреляция с разными значения --max-old-space-size (хотя отвечают они за разные участки памяти, и на метриках по идее от изменения первого параметра я должен увидеть влияние на GC minor, а от второго на GC major).

И тут легко попасть в ситуацию, когда увеличение работы которую нужно сделать GC не компенсируется уменьшением частоты работы GC.

Что я собственно и увидел в сегодняшнем кейсе c повышенным значением --max-old-space-size, где GC и major и minor работать стали реже в два раза, но тяжелее чуть ли не в десяток раз (прикладываю графики)
Привет!

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

Есть такая либа day.js, и ее система плагинов - https://day.js.org/docs/en/plugin/plugin

В этой документации рекомендованный подход к расширению - перезапись прототипа класса dayjs:

// overriding existing API
// e.g. extend dayjs().format()
const oldFormat = dayjsClass.prototype.format

dayjsClass.prototype.format = function(arguments) {
// original format result
const result = oldFormat.bind(this)(arguments)
// return modified result
}


У приложения - своя обертка над day.js где как раз есть такой плагин, перезаписывающий метод прототипа.

Далее, что мы имеем:
- эта обертка и сам day.js вынесены в shared Module Federation чанк
- хостовое приложение поставляет этот чанк
- в приложении загружается код микрофронта, который использует этот чанк

Код микрофронта имеет такой жизненный цикл на сервере:
- загружаем код микрофронта как строку
- компилируем строку и получаем JS модуль с экспортами
- сохраняем этот модуль в LRU кэш (микрофронтов может быть много, новые версии появляются, старые становятся не нужны)
- инициализируем Module Federation

Проблема - код микрофронта и его скомпилированный результат многократно дублируются в памяти, при этом прослеживается очень длинный трейс - ссылка на метод dayjs.

После долгой отладки мы обнаружили, что:
- код микрофронта в какой-то момент вытесняется из кэша и загружается заново
- код микрофронта инициализируется и вызывает shared модуль - обертку над day.js, что бы получить его экспорты
- shared модуль переиспользуется тот же самый, но заново выполняется код плагина через dayjs.extend(plugin)

Только на этом вызове, метод в прототипе const oldFormat = dayjsClass.prototype.format - это уже тот метод, который один раз мы заманкипатчили на предыдущем вызове плагина!

В итоге на каждое вытеснение микрофронта из кэша мы получаем матрешку перезаписи метода в прототипе, если визуализировать это в псевдокоде:
dayjsClass.prototype.format = myFnc
- dayjsClass.prototype.format = myFnc(myFnc)
- dayjsClass.prototype.format = myFnc(myFnc(myFnc))
- dayjsClass.prototype.format = myFnc(myFnc(myFnc(myFnc)))
...


Наглядный кейс про сайд-эффекты, shared зависимости и синглтоны.

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

Но добавляю к скрипту который запускает webpack сборку небольшого example приложения (соответственно считывает просто кучу модулей по пути, в том числе babel, его плагины и тд), и вижу либо отсутствие изменений либо ухудшение (штраф за формирование этого кэша).

При отладке через NODE_DEBUG_NATIVE=COMPILE_CACHE, логи показывают успешное переиспользование кэша.

У кого-нибудь есть успешный опыт интеграции NODE_COMPILE_CACHE?

Также, пока писал пост понял что при сборе CPU profile в Node.js не вижу сколько эта компиляция в принципе занимает времени, в отличие от обычной performance вкладки в девтулзах клиентских приложений, где есть время Compile code / Compile script. Можно ли это собрать для Node.js скрипта?
И раз уж зашел разговор о CLI, поделюсь одной из актуальных задач - разработка обновленной @tramvai/cli (уже писал про это короткий пост)

Во вложении - дизайн новой CLI, он уже претерпел ряд изменений, но основные концепции остались.

Какие основные цели для новой CLI:
- решить базовые проблемы с перформансом - основная, webpack MultiCompiler запускает все сборки в одном процессе, серверная и клиентская конкурируют между собой
- реализовать удобную систему плагинов (и первым же новым плагином интегрировать rspack)
- полностью разделить JS API и CLI API
- сделать общий набор тест-кейсов, который будет удобно запустить с разными плагинами - вебпак+бабель, вебпак+swc, rspack
- избавиться от легаси, улучшить отладку, упростить структуру

Итак, основная техническая задачка тут - ускорение двух параллельных вебпак сборок.

Тут очевидное решение - вынести их в worker_threads, что из коробки webpack и его MultiCompiler не умеет.

И главный челлендж тут - как передать конфигурацию из CLI в воркеры, если там могут быть плагины - то есть не сериализуемые методы/классы/прочие объекты?

Этот кейс решил следующим образом:
- есть общая логика - чтение tramvai.config.ts конфигурационного файла, где могут быть плагины
- есть набор входящих сериализуемых параметров, которые можно передать через CLI (`tramvai start ...`) или JS API (`new Tramvai().start(...)`)
- и основной процесс и webpack воркер - считывают один и тот же конфигурационный файл
- входящие параметры пробрасываются при старте воркера из основного процесса

Вокруг воркеров сделал небольшие удобные обертки для контроля и коммуникации.

Основной пакет @tramvai/api определяет базовые интерфейсы - DevServer и Builder, ждет их в DI контейнере, и запускает их жизненный цикл.

Вся логика с webpack, реализация DevServer на основе webpack-dev-middleware и соответствующие зависимости - в отдельном @tramvai/cli-plugin-webpack плагине, аналогичный будет для rspack.

Все babel зависимости и фабрика babel конфига - в отдельном @tramvai/cli-plugin-babel, и соответственно такой же будет для swc.

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

Похоже основным челленджем далее будет - миграция пользователей и временная поддержка двух реализация команды tramvai start.
Одна из классных идей в новой CLI - кастомные трейсы в формате Trace Event Format

Идея взята у Parcel, Rspack и Next.js, примеры:
- https://parceljs.org/features/profiling/#tracing
- https://github.com/parcel-bundler/parcel/blob/v2/packages/core/profiler/src/Tracer.js
- https://rspack.dev/contribute/development/tracing

Написал кастомный трейсер поверх либы chrome-trace-event, пример API:

const tracer = new Tracer();

tracer.wrap({ event: 'event' }, async () => {
await doSomethingAsync();
});


Во вложении пример визуализации кастомного трейса на сборку и несколько ребилдов, в интерфейсе https://ui.perfetto.dev/. Очень удобно смотреть сколько времени занимают основные операции, какие блокируют друг друга, где произошла ошибка (трейсы пишутся на диск не в конце а все время жизни скрипта)

В идеале - еще собирать более подробные трейсы по сборке через хуки бандлера.