Skip to content

perf(image): cap the panel's Node young generation at 16 MB semi-spaces - #122

Merged
agentHits merged 1 commit into
AgentHits-Devfrom
perf/node-young-generation
Oct 28, 2026
Merged

agentHits merged 1 commit into
AgentHits-Devfrom
perf/node-young-generation

Conversation

@agentHits

Copy link
Copy Markdown
Collaborator

Зачем

Node 26 выбирает размер молодого поколения кучи V8 по RAM хоста. На сервере с 4 ГБ полупространство получается 32 МБ, и под нагрузкой молодое поколение растёт до ~60 МБ. Короткоживущий мусор запросов панели помещается в меньший объём, а лишнее молодое поколение — это просто память сверху на каждом пике нагрузки.

Замер на рабочей панели (node -e в контейнере, тот же Node и те же 4 ГБ): молодое поколение без флага 59 МБ, с --max-semi-space-size=16 32 МБ.

Что изменилось

  • Dockerfile, финальный этап: ENV NODE_OPTIONS=--max-semi-space-size=16.

NODE_OPTIONS, заданный у сервиса, заменяет значение из образа. В простое V8 и без флага сжимает молодое поколение до 1–2 МБ, поэтому флаг срезает только пики.

Проверки

  • Стенд изолированный: Docker-in-Docker на Mac, нативный arm64-образ AgentHits-Dev. Сервис панели ограничен 3915 МБ, как RAM рабочего сервера: от этого Node берёт размер кучи (2054 МиБ) и молодого поколения.
  • Флаг проверялся переменной NODE_OPTIONS на том же образе: в Dockerfile ставится та же переменная. Образ этой ветки целиком на стенде не собирался; CI собирает его в main-image-smoke.
  • Прогоны шли вперемешку с другими вариантами. В таблице медиана по прогонам.
Без флага (3 прогона) С флагом (3 прогона)
Шаги сценария без ошибок 90 из 90 90 из 90
Молодое поколение, пик 61 МиБ 32 МиБ
Куча V8, пик 369 МиБ 351 МиБ
Память сервера панели, пик 532 МиБ 510 МиБ
Память сервера панели в простое 234 МиБ 235 МиБ
Память сервера панели после нагрузки и 2 минут простоя 349 МиБ 349 МиБ

Скорость в этих прогонах гуляла на ±20%, поэтому её проверили отдельно: 4 пары коротких прогонов вперемешку, средние по второму и третьему повтору сценария.

Без флага С флагом
Шаги сценария без ошибок 120 из 120 120 из 120
API, запросов в секунду (1000 запросов, 10 одновременно) 363 401
API, задержка 95% запросов 41 мс 33 мс
Страница проектов, запросов в секунду (200 запросов, 5 одновременно) 304 347
Процессорное время на сценарий 16,1 с 16,1 с
Память сервера панели, пик 518 МиБ 499 МиБ

Скорость с флагом не ниже; разница в её пользу меньше разброса между прогонами, поэтому выигрышем её не считаем. Пик памяти ниже в каждом прогоне: с флагом 496–500 МиБ, без флага 503–526 МиБ.

Сценарий: вход; пять страниц панели; 10 запросов project.all подряд; 1000 запросов к API по 10 одновременно и 200 к странице проектов по 5; проект; деплой Compose со сборкой и живым логом; логи и терминал контейнера; деплой из образа; добавление сервера и SSH-терминал.

Что проверить вручную на dev

  • После выкладки node -p process.env.NODE_OPTIONS в контейнере панели показывает --max-semi-space-size=16, панель открывается, деплой проходит.

🤖 Generated with Claude Code

Что:
- В образе панели `ENV NODE_OPTIONS=--max-semi-space-size=16`. Полупространство молодого поколения кучи V8 ограничено 16 МБ, молодое поколение под нагрузкой растёт до 32 МБ вместо ~60.
Зачем:
- Node 26 выбирает размер молодого поколения по RAM хоста. На сервере с 4 ГБ оно под нагрузкой растёт до ~60 МБ, а короткоживущий мусор запросов панели помещается в меньший объём.
- На изолированном стенде пик памяти сервера панели под нагрузкой ниже на 19–22 МБ (около 4%), скорость и процессорное время не изменились.
Риски:
- Сборки молодого поколения идут чаще; на стенде это не видно ни по процессорному времени, ни по скорости. В простое V8 и без флага сжимает молодое поколение, поэтому память в простое не меняется.
- `NODE_OPTIONS`, заданный у сервиса, заменяет значение из образа.
Проверки:
- Команды и результаты: на сервере с 4 ГБ `node -e` в контейнере панели: молодое поколение без флага 59 МБ, с `--max-semi-space-size=16` 32 МБ. Стенд: нативный arm64-образ `AgentHits-Dev` в Docker-in-Docker с `NODE_OPTIONS` и без него, лимит памяти сервиса 3915 МБ как на сервере; три прохода сценария по 3 прогона и отдельная проверка скорости из 4 пар прогонов вперемешку.
- Ограничения: стенд на Mac, а не на сервере; образ этой ветки целиком не собирался, флаг проверен переменной окружения на образе базовой ветки.

What:
- The panel image sets `ENV NODE_OPTIONS=--max-semi-space-size=16`. The V8 young-generation semi-space is capped at 16 MB, so under load the young generation grows to 32 MB instead of ~60.
Why:
- Node 26 sizes the young generation from the host's RAM. On a 4 GB server it grows to ~60 MB under load, while the panel's short-lived request garbage fits in less.
- On the isolated bench, the panel server's peak memory under load is 19–22 MB lower (about 4%), with unchanged speed and CPU time.
Risks:
- Young-generation collections run more often; the bench shows no difference in CPU time or speed. When idle, V8 shrinks the young generation even without the flag, so idle memory does not change.
- A `NODE_OPTIONS` set on the service replaces the image value.
Checks:
- Commands and results: on a 4 GB server, `node -e` in the panel container: young generation 59 MB without the flag, 32 MB with `--max-semi-space-size=16`. Bench: a native arm64 `AgentHits-Dev` image in Docker-in-Docker with and without `NODE_OPTIONS`, service memory limit 3915 MB as on the server; three scenario passes of 3 runs and a separate speed check of 4 interleaved run pairs.
- Limitations: the bench runs on a Mac, not the server; the image of this branch was not built as a whole, the flag was checked through the environment variable on the base branch image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@agentHits
agentHits merged commit 4412e1e into AgentHits-Dev Oct 28, 2026
7 checks passed
@agentHits
agentHits deleted the perf/node-young-generation branch October 29, 2026 00:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant