Полтора года назад я выступал на FrontendConf и потратил 40 минут на профайлинг.
Перечисленные приемы и инструменты по-прежнему актуальны — сегодня публикую видео с подробным изложением.
Доклад расскажет, что такое профилирование, научит локализовать потенциальные утечки памяти, а также немного углубит понимание инструмента DevTools. - Всем привет. Меня зовут Артём Несмиянов, я full-stack разработчик в Яндекс.
Практике.
И, как видите, сегодня я хочу поговорить о профилировании Node.js, хотя это не совсем фронтенд-тема.
Но сейчас очень много приложений используют frontback, у которого есть собственный рендеринг на стороне сервера, где всё нужно отдать клиенту, а фронтендёру часто приходится взаимодействовать с Node.js. Иногда случаются вещи, которые могут повлиять на ваш сервер, привести к его сбою, перегрузке и так далее.
Мы должны с этим бороться.
Я хочу показать, какие методы мы использовали.
Это скорее введение в профилирование Node.js. Зачем вообще нужно профилирование? Главным образом для того, чтобы ваш сервер был оптимизирован так, чтобы он не потреблял много процессорного времени и не потреблял много памяти.
Если потребляется много процессора, то потребляется много денег.
Это невыгодно для любого бизнеса.
Поэтому вам всегда следует следить за своими машинами и стараться, чтобы все было очень последовательно и ровно.
Начнем с ситуации, которая у нас возникла.
Подключив мониторинг, мы увидели, что возникла стандартная проблема: очень сильно скачет использование памяти.
Это очень плохо для нашей службы, надо что-то делать.
Машин четыре, и выделенная память постоянно скачет вверх-вниз, вплоть до 4 ГБ.
Каким должно быть приложение, чтобы оно использовало 4 ГБ?
Очевидно, память начинает ухудшаться только тогда, когда машины падают. Вы можете жить с этим.
Машина упала, перезапустилась, никто ничего не заметит. Но на самом деле здесь есть очевидная проблема.
Если сервер перегружен, ответ от него будет довольно долгим.
Пользователю нужно подождать три секунды, целевая аудитория может быть отклонена.
И, как я уже сказал, это требует ресурсов.
ЦП тоже ведет себя очень непоследовательно, все выглядит довольно отвратительно, с этим надо что-то делать.
Очевидно, здесь имеет место утечка памяти.
Думаю, нет смысла подробно объяснять, что это такое — короче, это причина, по которой память не возвращается обратно, не освобождается из кучи или вообще не используется.
Таким образом, приложение просто весит немного больше.
Почему может произойти утечка памяти? В первую очередь из-за случайной глобальной переменной.
Это также может произойти из-за ссылок на большие объекты.
Также иногда случается, что таймеры и короткие замыкания приводят к утечкам памяти.
Попробуем пройти перечисленные этапы немного подробнее.
Примеры будут относиться как к фронтенду, так и к бэкенду, потому что, как я уже говорил, существует рендеринг на стороне сервера, что тоже может создавать утечки памяти.
Одни и те же утечки могут возникать как на сервере, так и на фронтенде.
Нужно следить за обеими сторонами.
Итак, случайная глобальная переменная может возникнуть тогда, когда мы просто не объявили переменную в функции.
Bar, const илиled не были указаны.
В этом случае произойдет следующее.
Он будет присвоен либо окну, если мы на фронтенде, то есть код выполняется в нашем браузере, либо глобальному на Node.js. Такой элемент никогда не будет удален из памяти.
Он будет висеть там вечно, пока мы явно не сообщим об удалении этого атрибута.
Существует основная проблема, с которой новички часто сталкиваются при написании кода.
Он заключается в том, что вызывается метод класса, он сохраняется в другой переменной и вызывается без контекста.
Таким образом, здесь будет тот же глобал или окно.
Чтобы этого избежать, нужно, конечно, использовать strict. Он укажет на ваши ошибки.
Но иногда случается, что люди этим не пользуются, хотя это очень плохо.
Еще один интересный пример — ссылки на большие объекты.
Во фронтенде иногда нам нужно поместить элементы DOM в объекты, сохранить их там и получить к ним доступ с помощью ссылок.
В React вы можете создать ссылку React и добавить туда элемент. Допустим, вы хотите удалить его из DOM — конечно, используя RemoveChild. Но у вас все равно будет ссылка на этот объект. И до тех пор, пока bar не будет очищен или для anvar не будет установлено значение null, этот объект, который может быть очень большим, будет храниться в памяти.
Это не очень хорошо.
А если вы перестанете чем-то пользоваться, то за этими вещами нужно следить.
В свое время была еще одна популярная ошибка, приводившая к утечкам памяти — циклические ссылки.
Мы можем создать объект, содержащий очень большой массив из миллиона элементов, и зациклить его на самом себе.
Проблема в том, что раньше это делалось как сборка мусора.
Это то, что играет внутри вашего процесса и освобождает всю ненужную информацию, чтобы избавиться от ненужной памяти.
А раньше сборка мусора определяла, что данный объект не нужен в памяти только тогда, когда на него не было ссылок.
На предыдущем слайде элемент закрылся сам; всегда будет одна ссылка на один и тот же элемент. Такой объект, если мы будем его постоянно вызывать, просто останется в памяти.
Это не очень хорошо.
Следующий пример.
Мы будем вызывать функцию утечки каждую секунду, каждые пять секунд мы будем пытаться вызвать сборку мусора, а после десятой секунды перестанем вызывать функцию утечки.
Чтобы использовать сбор мусора, нам нужен флаг --expose-gc для Node.js. Вот что произойдет: Там, где есть столбец JC, видно, что произошла сборка мусора.
Есть также несколько показателей, о которых я расскажу позже.
Самая важная метрика здесь — HeapUsed. Видно, что начальный размер приложения составляет 4 МБ.
Потом получается 17 Мб, 23 Мб и так далее.
Объект все еще хранится в памяти.
Но после того, как мы отсчитаем интервал, все объекты исчезнут. Получается, что обратно возвращается 4 МБ.
Почему это происходит? Фактически, пример счетчика ссылок использовался ранее.
Я не зря написал IE8; раньше это была популярная ошибка.
Теперь все гораздо лучше продумано, и есть алгоритм сбора мусора, который называется mark and Sweep. Суть его заключается в следующем.
Есть корневой набор элементов, например window. Корневой набор — это то, из чего он будет искать элементы.
Очевидно, это оконные или глобальные переменные, а также все локальные переменные в текущем стеке вызовов.
То есть, если в данный момент запущена сборка мусора и сделаны вызовы, то он возьмет все вызовы, достанет оттуда все переменные, которые сможет получить, и из этих переменных попытается построить, грубо говоря, BFS, глубину- первый поиск, поиск в ширину.
Он возьмет корневые элементы, попытается охватить все из них, на которые есть ссылка из корневого элемента, и так далее.
То есть он просто сможет обойти случай, когда есть элементы, закрывающие друг друга или самих себя, при получении одной или нескольких ссылок.
Да, это требует немного больше времени, это очень простой счетчик ссылок.
Но это работает гораздо эффективнее.
Это очень упрощенный алгоритм работы сборки мусора.
Эти две ссылки ( 1 , 2 ) потом вы сможете ознакомиться со всеми техниками более подробно.
Главный принцип, упрощающий жизнь со сборкой мусора, — это распределение памяти на новое и старое поколения.
Новое поколение — это объекты, которые только что были созданы с использованием new. Старое поколение — объекты, пережившие около двух-трех циклов сборки мусора.
На этих элементах он будет вызываться гораздо реже.
Поэтому для каждого поколения выделяется область памяти, и за счет этих поколений он сам понимает, когда его нужно вызвать.
Но, как я уже сказал, в наших примерах мы сами будем вызывать сборку мусора, чтобы явно отслеживать, как очищалась память.
Итак, вернемся к нашим примерам.
Еще одна из основных ошибок, из-за которой могут возникнуть утечки памяти, — это, конечно же, таймеры.
Очевидная проблема заключается в том, что мы просто не отпускаем функцию, когда забываем очистить интервал.
Здесь мы создаем очень большой объект. Создается таймер, который будет выполнять сложные операции и пытаться реализовать наш большой массив.
Это не самая простая операция, она сама требует дополнительной памяти, а сам таймер, пока не будет освобожден, всегда будет содержать в контексте переменную bigBar. Как вы понимаете, пока мы его не почистим, он всегда будет лежать внутри нашей стопки.
Давайте попробуем запустить наш таймер и ничего не делать в течение 10 секунд и посмотрим, что произойдет. Здесь видно, что пока мы не очистим таймаут, на этот объект будет потрачено очень много памяти.
На наш массив выделено 11 мегабайт, и вы можете видеть, что иногда он выскакивает за пределы 20 мегабайт. Это означает, что только JSON.stringify, стерилизация нашего массива, также в этот момент содержит данные, замыкания, что-то, что ему нужно.
Но потом, судя по всему, ей удается их почистить.
То есть JSON.stringify работает более-менее хорошо.
Но хранить объекты памяти само по себе не очень хорошо.
Если мы когда-нибудь забудем почистить этот таймер, он продолжит зависать.
Об этом не следует забывать.
Примерно такая же проблема возникает и в следующем случае.
Вот довольно интересный пример.
Есть внешняя функция, также создается интервал, и каждый раз в интервале мы сохраняем результат работы внешней функции в переменную res.
Внутри внешней функции мы создаем очень большой массив и переменную, содержащую ссылку на предыдущий результат. В первый раз, конечно, оно будет неопределенным, а во второй раз — результатом выполнения.
Не приведет ли это к утечкам памяти? Что, если мы добавим сюда такую функцию? Это все довольно интересно.
Давайте сначала посмотрим, что произойдет.
Каждый раз, когда мы вызываем внешнюю функцию, она создает новый объект. И каждый новый объект будет сохраняться в памяти, тем самым предотвращая освобождение предыдущего объекта, ведь у нас есть ссылка на предыдущий результат.
Почему это происходит? По сути, не имеет значения, в какой функции находились переменные oldRes bigData. Движок V8 просто просматривает ваш код, собирает все функции, которые можно вызвать внутри этой функции, и собирает контекст.
Допустим, здесь была еще одна переменная, которая нигде не использовалась.
Если бы вы установили точку останова и достигли этой точки, вы бы увидели, что там будет ошибка ссылки.
Вы не сможете увидеть, что находится на этом объекте.
Но если внутри вашей функции есть хотя бы одна из функций, использующая эту переменную, она создаст контекст для всех функций.
Вы можете сделать точку останова внутри этой функции, если она когда-либо будет вызываться, посмотрите из контекста на bigData и oldRes, потому что они тоже останутся внутри контекста.
Это потрясающе.
С одной стороны, понятно, почему разработчики на это пошли.
С другой стороны, это не очевидно.
И нужно просто понимать, как это работает, чтобы не создавать подобных вещей.
Поймите, что иногда функция, которую вы даже не вызываете, может создавать проблемы, утечки памяти.
Конечно, примеры очень синтетические; на производстве найти такие вещи бывает гораздо сложнее, но они могут возникнуть и просто по невнимательности.
Давайте посмотрим, что мы можем сделать, чтобы профилировать и дразнить.
Какие инструменты для этого существуют? Самый распространенный и интересный инструмент, которым все злоупотребляют, — console.log. На самом деле это очень мощный инструмент. Фронтендеры особенно любят куда-нибудь воткнуть один, два, три и посмотреть, вызывались ли там эти вызовы или определенные функции или нет. И, таким образом, понять, в чем проблема.
Да, нет времени запускать режим отладки, нет времени проверять пошагово.
Хотя на самом деле лучше было бы сделать это с помощью отладки.
Но иногда эта вещь может оказаться очень полезной.
Чтобы увидеть еще больше интересных инструментов, давайте посмотрим, что еще может сделать для нас объект глобальной консоли.
Мы можем посмотреть трассировку стека.
В консоли есть встроенный метод Trace, который даст нам текущую трассировку стека, но она будет ограничена определенным количеством строк.
Это будет зависеть от Node.js или вашего браузера.
У этого подхода есть еще одна проблема — трассировка стека будет возвращена в виде строки.
Вы не сможете его разобрать, вы не сможете получить его как объект и что-то с ним сделать.
Но вы можете получить его как строку.
Если хотите, можете его разобрать.
Конечно, это дело очень странное, но иногда оно полезно.
Кроме того, мы можем использовать newError.stack, указать Node.js параметр stack-trace-limit и количество строк, которые мы хотим вывести.
Здесь это тоже будет строка, но мы можем создать собственный генератор трассировки стека.
Есть такая штука — подготовитьStackTrace. Есть даже целый API трассировки стека, вы можете хорошо разбираться в трассировке стека, брать конкретные объекты сайта вызова, которые позволят вам получить важную информацию, чтобы войти в нее.
Это больше не будет той же самой трассировкой стека.
.
но гораздо более информативно.
Описывать это можно как угодно — грубо говоря, просто переопределив эти методы.
stackTraceLimit очевиден, и captureStackTrace также собирает для вас трассировку стека.
Ознакомиться с API трассировки стека можно по адресу связь .
Там есть довольно интересные примеры.
И вообще, лучше понять, как правильно и вообще работать со Stack Trace.
Еще один очень важный аспект профилирования и, в принципе, оценки вашего приложения — это, конечно, сроки выполнения ваших функций.
Иногда вам следует просто рассчитать время выполнения своих функций, убедиться, что они не занимают слишком много времени.
Все используют это во внешнем интерфейсе.
Но на бэкенде есть более сложные вещи.
Поговорим о них немного дальше.
Но есть гораздо более простые вещи, которые позволяют вам просто выполнить console.time и console.timeEnd. В console.time мы передаем аргумент — имя нашей группы.
Мы просто задаем имя, под которым будет собираться информация.
В timeEnd консоль отобразит информацию о том, сколько времени потребовалось для выполнения этой функции.
В чем недостаток этого подхода? Вам придется сразу перейти к коду и установить эти обработчики.
Иногда это даже можно сделать, если зайти в DevTools во время выполнения и изменить его.
Но иногда это накладывает ограничения.
Это не очень удобно.
Также существует проблема с асинхронностью.
Если функция, генерирующая timeEnd, вызывается асинхронно и ее можно вызывать несколько раз, вы больше не увидите, сколько времени занял второй запрос.
Вы увидите только первое, дальше вам ничего не будет показано.
Существуют гораздо более продвинутые API для измерения производительности вашей страницы или процесса Node.js. Есть те же методы, например мера.
Он позволяет вам делать то же самое, что и time и timeEnd. Timerify — это вообще просто декоратор для вашей функции, который позволит вам сразу измерить, как долго работает ваша функция — то есть не от и до какого-то момента, а полностью.
И есть nodeTiming. Внутри есть много разных методов, таких как LoopStart, атрибуты, которые, например, могут сказать вам, когда начался следующий такт вашего Land Loop. Иногда эти вещи полезны.
Например, Performance.now кажется странной вещью, но он показывает, как долго ваша программа уже работает. Вроде бы эти вещи не очень нужны, но иногда нам, как и всем разработчикам, нравится с чем-то поиграться, разобраться в метриках, узнать, как быстро что-то загружается.
Сейчас очень помогает в этом, учтите это.
Есть связь на производительности.
крючки.
Есть очень большой API, множество методов, которые позволят вам делать практически всё, измерять всё со стороны Node.js. Также есть оболочка для передней панели, где вы также можете посмотреть различные взаимодействия (например, как долго загружалась ваша страница) и получить все First Contentful Paints. В общем, все, что вы используете в DevTools, можно использовать здесь для измерения производительности.
Только Node имеет глобальный объект процесса.
Обычно он используется для получения переменных среды.
Это его самый стандартный случай.
Но есть еще атрибут MemoryUsage, который тоже позволяет получать некоторую информацию.
Помните те признаки, которые были, когда мы получили утечки памяти? Это параметры, которые там используются.
Давайте пройдемся по ним немного подробнее.
Как я уже говорил ранее, HeapUsed — это объем памяти, выделенный вашим конкретным объектам.
Вы создали массив в миллион символов, грубо говоря, на него выделено десять мегабайт. Мы только что создали ссылку — было выделено 64 килобайта.
Кажется, все ясно.
Но heapTotal, размер кучи, всегда оказывается немного больше, чтобы с ним было легко работать в памяти — чтобы было проще выделять объекты без постоянного выделения памяти.
Обычно HeapUsed меньше heapTotal. HeapUsed может быть немного выше heapTotal, но это очень редкий случай.
Это и есть процесс подкачки, момент увеличения размера кучи.
Есть еще два варианта — RSS и внешний.
Никто не должен заботиться о них, когда вы пытаетесь профилировать или опровергать свои приложения.
Но я их тоже немного затрону.
Внешний — это объем памяти, выделенный внутри самого движка.
C++ создает объекты для ваших элементов, оборачивает куда-то код и, возможно, создает какие-то строки — все это называется внешним.
Это также хранится в нашей куче.
О РСС.
Есть такое понятие – резидент. Это то, сколько ваш процесс весит в памяти.
Обычно цифра намного выше, потому что ему еще нужно загрузить все библиотеки, все зависимости и так далее.
RSS даст вам общий размер вашего приложения.
Обычно это не используется при профилировании или отладке; они смотрят только на HeapUsed, потому что самая главная тенденция — сделать так, чтобы ваши объекты занимали как можно меньше памяти и чтобы эта характеристика не росла бесконечно.
Поскольку мы все интерфейсные разработчики, мы не хотим устанавливать себе странное дополнительное программное обеспечение для профилирования.
DevTools может помочь нам во всем этом.
Профилировать Node.js мы можем через обычный DevTools, самый понятный для нас инструмент. Но мне бы хотелось отточить то, на что он способен.
Давайте рассмотрим, что на самом деле делают DevTools.
Прежде всего, с точки зрения внешнего интерфейса мы можем рассматривать и редактировать элементы DOM и правила CSS. Не все знают, но посмотреть все обработчики событий можно здесь, во вкладке Event Listeners, которые у нас сейчас висят на всех DOM-элементах.
Там вы можете делать много вещей; чуть позже приложу несколько ссылок, которые показывают скрытые возможности, не все о них знают. Например, вы можете делать снимки экрана своих элементов, вы можете сразу получить к ним доступ, не помещая элемент DOM в глобальную переменную и так далее.
Панель консоли.
Использовать его довольно просто, но не все используют некоторые функции.
Например, так: мы можем посмотреть, какие запросы были отправлены, увидеть все события, которые могут произойти с XMLHttpRequests. Допустим, мы хотим сделать это не во вкладке сети, а здесь.
Мы можем сгруппировать наши выходные данные в консоль, мы можем создать журнал сохранения, который позволит нам сохранять нашу консоль при переходе на другую страницу или ее хешировании.
Иногда это тоже полезно.
Мы можем фильтровать наши сообщения, например, по регулярному выражению.
Консоль также имеет множество различных атрибутов.
Например, console.log, кто не знал, тоже имеет второй аргумент. Вы даже можете указать стили для console.log, раскрасить сообщения, сгруппировать их, сделать знаки.
Тоже много чего.
Но я считаю, что нет смысла останавливаться на достигнутом.
Панель «Источники» — более важная для нас панель, которая позволяет вводить код во время выполнения, добавлять точку останова или условную точку останова.
По какой-то причине многие люди также не используют условные точки останова.
Они даже не знают, что если щелкнуть правой кнопкой мыши по строке, то там можно сделать условные точки останова и повесить их по определенным условиям, показать, останавливаться на этой точке останова или нет. Мы можем мониторить конкретную переменную, можем смотреть текущий стек вызовов, то есть стек вызовов.
Можем посмотреть Scope — что находится в контексте этой строки.
Я только что сказал об ошибке ссылки и наших больших объектах, что они будут видны внутри контекста.
Мы также можем прикреплять точки останова к определенным событиям пользователя.
Если вы прокрутите вниз, появятся точки останова прослушивателей событий.
Там можно прикрепить условие типа «при клике сделай точку останова и посмотри, в какой функции ты сейчас находишься».
Также есть такая возможность, как Покрытие.
Позволяет увидеть, какой код вообще выполнился, а какой нет. Вы можете видеть: те строки, которые уже были выполнены, отмечены зеленым, а те, которые еще не были выполнены, отмечены красным.
Например, мы ищем кнопку, на которую не нажимали, и видим, что этот код еще не выполнился.
Иногда это удобно, если нужно протестировать компоненты.
То же самое и с вашим сервером.
Вы можете увидеть, какой код вообще не выполнился.
Иногда можно легко найти вещи, которые вам вообще не нужны.
Так как мы прошли все панели, то есть, конечно, панель Сеть.
Мы можем изменить параметры кэширования или пропускной способности, можем вообще уйти в офлайн, перейти к дросселированию нашей сети — например, в 3G. Посмотрите, как будет работать ваш сайт, посмотрите на работоспособность, как все это делается, посмотрите подробную информацию по вашему запросу.
Для сервера это очень важно — видеть, какие заголовки были отправлены и всё ли в порядке.
Если у вас фронтбэк, то обычно через ваш сервис можно сделать какой-то прокси.
Поэтому часто важно понимать все аспекты использования сети.
И, конечно же, вы также можете фильтровать свои запросы.
Давайте двигаться дальше.
Более важная панель — «Производительность».
Там можно отслеживать все события, происходящие на странице, видеть, когда используется GPU, видеть стек вызовов, который был вызван за определенное время, можно указать это на временной шкале.
И вы можете увидеть, сколько времени потребовалось для выполнения вашего JavaScript-кода, сколько времени было выделено на рендеринг.
Это такой апогей между двумя вещами: можно смотреть и бэкенд, и фронтенд. С фронтендом, думаю, здесь мы все все понимаем.
В бэкэнде наиболее важной частью является дерево вызовов.
Но когда мы профилируем Node.js, панели «Производительность» по-прежнему нет. Там есть еще одна панель, о которой я расскажу чуть позже.
И, конечно, самое главное — панель «Память», она есть и та, и другая.
Давайте посмотрим на это более подробно.
Прежде всего, на панели «Память» вы можете увидеть все, что в данный момент находится в памяти.
Мы можем найти какой-то элемент. Здесь мне удалось найти в памяти ключи для нашего объекта Intel, который потом переведет наши ключи в обычные трансляции.
Мы видим, сколько он весит. У нас есть Shallow Size, Retained Size и сколько на самом деле весит приложение в тот момент, когда я решил сделать его снимок.
Есть еще несколько вкладок, о них мы поговорим чуть подробнее.
Здесь мы можем смотреть как на фронтенд и искать там утечки памяти, так и на бэкенд, то есть на Node.js.
Как я уже говорил, есть полезные вещи, о которых знают не все.
Мы можем немного углубиться в то, как использовать DevTools в целом.
Вот две ссылки на проглиб И Developers.google.com , которые хорошо описывают то, что программисты обычно не используют. Прочтите это, чтобы облегчить поиск проблем и облегчить работу с этим инструментом.
Вернемся к нашему синтетическому примеру с неявным замыканием.
Попробуем это профилировать.
Во-первых, нам нужно указать флаг проверки, который запустит соединение через веб-сокет и создаст мини-сервер, с которым мы сможем общаться: Мы видим, что наше приложение запустилось, мы можем профилировать его, зайдя в инспекцию или в хром://проверить .
Мы можем найти его там и переправиться.
Видим, что оно продолжается, можем перейти на вкладку «Память» и измерить наше приложение.
Все то же самое, что и во фронтенде.
Мы сделали остановку.
Видим, что приложение весит 4,6 МБ.
Мы можем попробовать обойти нашу кучу и посмотреть, что там.
Давайте измерим еще раз.
Мы видим, что Теги: #Оптимизация сервера #JavaScript #мониторинг #node.js #профилирование #chrome devtools
-
Как Сделать Резервную Копию Важных Данных
19 Oct, 24 -
Mandriva Install Fest В Екатеринбурге
19 Oct, 24 -
Субстики №140
19 Oct, 24 -
Автоматизация Тестирования «По-Китайски»
19 Oct, 24 -
Эссе По Проверке Данных
19 Oct, 24 -
Ботнеты И Их Виды: Что Известно В 2018 Году
19 Oct, 24 -
Прелюдия Или Как Полюбить Haskell
19 Oct, 24