Головна / Статті / Буфер кешу карт джерела Node є тихим витоком пам’яті у режимі розробки.

Буфер кешу карт джерела Node є тихим витоком пам’яті у режимі розробки.

Дізнайтеся, чому увімкнення параметрів --enable-source-maps або NODE_V8_COVERAGE може спричинити необмежене зростання пам’яті через постійні виклики eval, та як сьогодні діагностувати та усунути цю проблему.

1073 слів

Процес Node виглядає абсолютно здоровим одразу після запуску, але поступово займає все більше пам’яті під час постійної редагування коду. Якщо цей процес було запущено з параметром --enable-source-maps (або з встановленим значенням NODE_V8_COVERAGE), і щось у вашій стек-структурі постійно викликає функцію eval з новим значенням //# sourceURL щоразу, то, ймовірно, ви знайшли причину: сильний кеш Source Map, який накопичує створені записи та ніколи їх не звільняє. Об’єм пам’яті продовжує зростати. Ви перезапускаєте процес — він знову росте.

Ви намагаєтесь примусово виконати збір сміття. Ви очищуєте всі можливі посилання. Проте об’єм пам’яті все одно продовжує зростати.

Патерн зростання

Уявіть собі сервер розробки, який працює протягом певного часу, або будь-який процес Node з довгим терміном існування, який генерує штучні стек-трейси чи „фрейми власника“ з позначками унікальних URL джерел. Як RSS, так і heapUsed постійно зростають. Перезапуск процесу скидає ці показники, але виконання тієї самої роботи без флага source-map зберігає обсяг пам’яті на рівному рівні.

У публічному системі відстеження проблем Node.js (nodejs/node#65760) для демонстрації цієї поведінки використовується мінімальний приклад, який обробляє близько 90 байтів коду за допомогою одного зовнішнього мапування джерел, змінюючи лише sourceURL при кожній ітерації. Після виконання примусового збирання сміття:

Evals | with --enable-source-maps | no flag
0     | 5 MB                       | 5 MB
400   | 170 MB                     | 5 MB
800   | 334 MB                     | 6 MB
1200  | 499 MB                     | 6 MB

Це означає приблизно 415 КБ пам’яті, які залишаються після кожного виклику eval, причому верхня межа не видно.

Якщо ви працюєте з Next.js App Router, це часто проявляється у тому, що команда next dev збільшується на десятки мегабайт після кожної зміни файлу. Механізм owner-stack у React Server Components виконує eval один раз на кожну стекову рамку, позначаючи кожен виклик чимось на кшталт //# sourceURL=about://React/…?<counter++> разом із великим вбудованим картографуванням джерела. Оскільки цей лічильник збільшується при кожному виклику, кожен ключ кешу є унікальним, тому нічого не використовується повторно. Відповідна тема обговорень у Next.js — vercel/next.js#98221; вважайте її місцем, де проявляється цей симптом, а не окремою причиною.

Чому кеш не дозволяє зберегти дані

Прапорець --enable-source-maps наказує Node зберігати у кеші Source Maps, щоб стек-трейси під час виконання можна було перетворити назад у ваші оригінальні вихідні файли (див. документацію CLI). Вихідні коди звичайних модулів зберігаються у кеші з слабкою ключованістю, тому колектор сміття може їх звільнити, як тільки вони більше не використовуються. Однак генеровані коди — ті, якими керує гілка isGeneratedSource — потрапляють у generatedSourceMapCache, простий кеш типу Map із сильною ключованістю, визначений у файлі lib/internal/source_map/source_map_cache.js, який ніколи не очищується.

Коментар до коду, пов’язаний із цим кешем, передбачає, що протягом життя процесу буде лише кілька створених джерел. Механізми Hot Module Replacement та оновлення власного стеку повністю руйнують це припущення. Кожен унікальний sourceURL стає постійним ключем, старі записи ніколи не видаляються, і повна розібрана карта для кожного з них залишається у пам’яті.

Встановлення NODE_V8_COVERAGE спрямовує дані через саме цей кеш, тому ви будете спостерігати такий самий необмежений ріст, коли ключі eval продовжуватимуть змінюватися.

Вже існує відкрита заява про pull request у основному репозиторії (#65761), яка обмежує кеш generated-sources за допомогою стратегії LRU з використанням байтового бюджету — 32 МБ у найновішій версії — та оновлює записи під час читання, щоб карти для досі активних генерованих функцій не видалялися передчасно. Наразі ця заява залишається відкритою та позначена як needs-ci. Її ще не об’єднали, і вона не є частиною жодної оприлюдненої версії Node, тож не варто припускати, що ваша поточна LTS-версія вже містить це виправлення.

Підтвердження діагнозу

  1. Перевірте, чи ваш процес з довгим часом роботи запущений із параметром --enable-source-maps або чи встановлений параметр NODE_V8_COVERAGE, і чи щось постійно виконує код за допомогою eval з кожним разом новим значенням //# sourceURL — це може бути HMR, стеки власників RSC або користувацький кодгенератор.
  • Зразок значення process.memoryUsage().heapUsed після виконання циклу примусового очищення пам’яті, як у окремому сценарії запуску за допомогою node --expose-gc, так і шляхом створення знімка пам’яті через інспектор у діючому процесі.
  • Запустіть ту саму роботу без цього флага. Ознакою, зазначеною у #65760, є те, що обсяг пам’яті залишається стабільним без флага, а з його увімкненням поступово зростає.
  • За бажанням можна створити знімок пам’яті та перевірити його на наявність елементів generatedSourceMapCache або context:generatedSourceMapCache. Дослідники проблеми виявили тисячі збережених записів, які утримували понад гігабайт даних sourcesContent та mappings під час справжньої сесії next dev.
  • Підвищення значення --max-old-space-size не є рішенням — воно лише відкладає неминучу збій через нестачу пам’яті.

    Що робити прямо зараз

    Виберіть підхід, який підходить вашій конфігурації:

    1. Для проектів Next.js дійте негайно: запустіть next dev --disable-source-maps. За даними користувачів з треду #65760, обсяг даних значно зменшився — приблизно +6 MB за кожну зміну порівняно з приблизно +89 MB за наявності карт джерела, згідно з їхніми вимірюваннями. Читабельність стек-трейсу знижується, але комп’ютер більше не закінчує пам’яті.
    2. Для інших інструментів Node, які працюють довго: видаліть параметр --enable-source-maps або скасуйте налаштування NODE_V8_COVERAGE у будь-якому сервері гарячого завантаження, поки картування стек-трейсів справді не стане необхідним. Краще використовувати картування джерела лише під час короткочасних сеансів відладки.
  • Стежте за виправленням у вихідному коді: слідкуйте як за записом nodejs/node#65760, так і за запитом на пул-ріквест #65761. Як тільки випуск Node прямо згадує про обмежений кеш генерованих джерел коду, можна безпечно оновлюватися. До того часу будь-які цифри від інших користувачів слід розглядати як показники їхньої конкретної налаштовки, а не як гарантію того, що ваше застосування буде поводитися так само.
  • Ігноруйте пораду „просто підвищити ліміт пам’яті“. Основний кеш є міцним та необмеженим при саме цій комбінації флагів та способу використання. Більший ліміт пам’яті дає лише трохи більше часу до збою.
  • Висновок

    Ваш сервер розробки не споживає пам’ять випадково без причини. Поєднання параметра --enable-source-maps із багаторазовими виконаннями коду, які використовують унікальні значення sourceURL, призводить до заповнення кешу generatedSourceMapCache, який ніколи не звільняє свої записи. Звичайні карти джерела модулів можна очищати за допомогою процедури garbage collection; створені автоматично карти — ні, принаймні доки не буде випущена версія з номером #65761. Вимкніть карти джерела під час процесів гарячого завантаження вже сьогодні та плануйте оновлення, як тільки з’явиться кеш із обмеженими ресурсами.

    Тож наступного разу, коли ви помітите, що об’єм пам’яті RSS поступово зростає під час простого редагування файлів, а процедура примусового очищення пам’яті нічого не допомагає, перевірте, чи не є причиною саме цей параметр.

    Пов’язана література