Типичная умная колонка с Алисой — это скромное железо и совсем немного оперативной памяти: у младших моделей её всего 256 МБ на всё устройство сразу. И за каждый мегабайт идёт настоящая борьба между множеством команд и компонентов: бизнес‑логикой, обработкой звука, локальными нейросетями.

Ежедневно мы собираем и анализируем memory‑дампы с миллионов колонок — нам важно понимать, куда уходят эти драгоценные мегабайты.

Меня зовут Сергей, я работаю в команде, которая пишет прикладной код на C/C++ для умных устройств с Алисой (например, колонок, телевизоров и автомобилей). Под катом расскажу, как мы построили и используем инструмент для анализа потребления памяти в проде на нашем флоте Linux‑колонок.


Сколько RSS доступно

Linux‑колонками мы называем колонки без Android: Станцию Лайт, Мини, Миди, Станцию 3, Стрит и другие. Под капотом у них полноценный Linux, но железо относительно слабое.

Для младших колонок: у главного процесса, который отвечает за бизнес‑логику, порядка 55 МБ resident set size (RSS — размер страниц памяти, выделенных процессу и находящихся в ОЗУ). Распределяется этот размер так: около 30 МБ занимает RSS file (память под файлы — код, замапленные ресурсы), а на анонимную память (RSS anon) — стеки и динамические аллокации — остаётся всего 25 МБ. И в эти 25 МБ должно уместиться всё разом: бизнес‑логика, аудиобуферы, обработка звука (VQE, voice quality enhancement) и локальные нейросети — например, споттеры (детекторы слова активации «Алиса» и других) и голосовая биометрия (узнавание пользователя по голосу).

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

Как следить за памятью «дёшево»

Дальше в статье сосредоточимся на динамических аллокациях (malloc/new) — главном потребителе RSS anon.

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

Инструментов, которые перехватывают malloc/free, хватает — например, Perfetto + heapprofd или Perforator. Но для колонок они слишком тяжёлые. Perfetto + heapprofd требует нескольких мегабайт памяти и места на флешке, а Perforator ещё тяжелее и не работает на 32-битном ARM.

Другой путь — заменить системный аллокатор на такой, в который отслеживание аллокаций встроено изначально. Кандидатов несколько: из популярных мы смотрели на TCMalloc и jemalloc.

Выбрали jemalloc. Это популярный аллокатор, спроектированный с упором на минимизацию фрагментации; вдобавок он умеет собирать семплированные дампы, гибко настраивается и — в отличие от TCMalloc — работает на 32-битных системах. А это, напомню, почти все наши колонки, так что фактор оказался решающим.

Системный аллокатор мы заменили на jemalloc — переход на всех Linux‑колонках занял несколько месяцев. А вот во что эта замена обошлась по ресурсам (с включённым семплированием в главном процессе): 

  • +350 КБ к размеру исполняемых файлов; 

  • +1% CPU;

  • 0 МБ памяти в сумме по всем процессам. 

Эти цифры очень сильно зависят от конфигурации аллокатора: на младших колонках память для нас важнее, поэтому мы использовали jemalloc с одной ареной и без thread‑кешей. Это и позволило выйти в ноль по памяти в среднем. То есть инструмент для анализа памяти в итоге сам этой памяти и не занял — что при нашем бюджете было критично.

Как работает семплирующий аллокатор

Механика такая. У аллокатора есть внутренний счётчик:

  • при каждом malloc счётчик уменьшается на размер выделяемой памяти;

  • как только он доходит до нуля, аллокатор сохраняет семпл — стек вызовов и размер текущей аллокации;

  • после этого счётчик сбрасывается в случайное значение со средним N байт.

N — это параметр семплирования, и обычно его задают степенью двойки. В jemalloc он называется lg_prof_sample; по умолчанию мы используем lg_prof_sample = 20, то есть N = 1 МБ. Значение намеренно крупное — именно оно делает семплирование дешёвым по памяти и CPU.

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

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

Как выглядит дамп памяти на примере jemalloc

Дамп jemalloc — это обычный текстовый файл с информацией о том, в каких стеках сколько памяти выделено. Вот сокращённый пример:

heap_v2/1048576
  t*: 41: 10482 [0: 0]
  t0: 10: 2142 [0: 0]
  ...
  t28: 1: 451990 [0: 0]
  ...
@ 0xab5632bc 0xab56328a 0xab55d906 0xab53a7ce
  t*:  2: 2460 [0: 0]
  t10: 1:  661275 [0: 0]
  t16: 1:  448    [0: 0]
@ 0xab5632bc 0xab56328a 0xab55d906 0xab53a7ce 0xac4f93d4 0xab4fbc6a 0xab5e5040 0xabcbc668 0xabcbc544 0xabcbd280 ... 0xabaecee0 0xabaece84 0xabe6c2dc 0xabe6f0a2
  t*:  1: 20461 [0: 0]
  t34: 1: 20461 [0: 0]
...
MAPPED_LIBRARIES:
aabaa000-ac4fe000 r-xp 00000000 00:10 2220    /opt/our_main_binary
ac50d000-ac60b000 r--p 01953000 00:10 2220    /opt/our_main_binary
ac61a000-ac643000 rw-p 01a50000 00:10 2220    /opt/our_main_binary
ac643000-ac6c0000 rw-p 00000000 00:00 0       [heap]
...
f6646000-f6724000 r-xp 00000000 00:10 109     /lib/libc-2.30.so
f673a000-f674c000 r-xp 00000000 00:10 116     /lib/libpthread-2.30.so
...
ec22b000-ec244000 r--s 00000000 00:11 123699  /.../spotter_model.nnet
...

Разберём его по частям.

Заголовок heap_v2/1048576 — версия формата и параметр семплирования в байтах (1 048 576 = 2²⁰ = ровно наш 1 МБ).

Накопительная сводка в начале (строки t*, t0, t1, …) — статистика по каждому потоку, где t* — суммарно по всем: сколько сейчас живых семплированных аллокаций и сколько суммарно семплированных байт в них. Тут можно заметить странность: в потоке t28 выделено больше, чем суммарно по всем потокам в t*. Это не ошибка: размеры здесь вероятностные, и брать их напрямую нельзя. Почему так — разберём в следующем разделе.

Стеки. Каждая запись начинается со строки @ 0xADDR 0xADDR … — это сырые адреса возврата (стек вызовов от malloc до точки входа) в том виде, в каком их видит процесс. Чтобы превратить их в имена функций, нужны дебаг‑символы соответствующего исполняемого файла. 

Под строкой @ … идут уже знакомые t*/t0/t1/…, но относятся они не к процессу целиком, а к этому конкретному стеку: сколько в нём живых аллокаций и сколько байт через него выделил каждый поток. Например, в длинном стеке выше t34: 1: 20 461 читается как «у потока t34 одна живая семплированная аллокация через этот стек размером 20 461 байт».

Числа [0: 0] в квадратных скобках — статистика по освобождённым аллокациям. У нас она выключена, мы следим только за живыми.

MAPPED_LIBRARIES — копия /proc/self/maps‑процесса: в каждой строке диапазон адресов и маппинг, к которому он относится. В первую очередь нам интересны маппинги с правами r‑xp (исполняемый код — например, из /lib/libc-2.30.so). Без них по сырому адресу 0xab5632bc не понять, к какому файлу он относится, не посчитать смещение внутри файла и не символизировать адрес — то есть не превратить его в имя функции и номер строки в исходниках. Но полезны и другие маппинги:

  • r--p — read‑only‑данные: константы, строки. Эти страницы пойдут в RSS file, если к ним обратятся.

  • rw‑p — read‑write‑данные: глобальные переменные. При записи страницы попадут в RSS anon.

  • r--s — read‑only‑файлы с данными: например, нейросетевые модели. Дают вклад в RSS file.

Математика: вероятность семплирования и интерпретация дампов

Процесс семплирования хорошо описывается экспоненциальным распределением. Пусть X — размер аллокации, S — параметр семплирования (у нас S = 1 МБ). Тогда вероятность того, что аллокация размера X создаст семпл:

P(X, S) = 1 − exp(−X / S)

Несколько конкретных значений для S = 1 МБ:

Размер аллокации X

Вероятность создания семпла

1 КБ

0,1%

64 КБ

6%

256 КБ

22%

512 КБ

39%

1 МБ

63%

4 МБ

98%

Отсюда видно главное свойство такого семплирования: крупные аллокации почти всегда попадают в дамп, мелкие — крайне редко.

Как интерпретировать один семпл. Допустим, в дампе есть семпл со стеком и размером X = 1 КБ. Вероятность поймать такую аллокацию — всего 0,1%. Значит, чтобы в среднем получить один такой семпл, нужно сделать примерно 1 КБ / 0,1% ≈ 1 МБ подобных аллокаций — то есть около 1000 аллокаций по 1 КБ. Поэтому при анализе мы домножаем размер каждого семпла на коэффициент 1 / P(X, S) и получаем взвешенный размер — оценку реально выделенной памяти.

Можно ли верить взвешенному размеру? Если данных мало, а аллокации мелкие — нет. По одному семплу в 1 КБ невозможно отличить «было 1000 мелких аллокаций — одну поймали» от «была одна‑единственная аллокация, которой повезло попасть в семпл». Но всё меняется, когда мы объединяем дампы с тысяч устройств: случайный шум усредняется, а реальные горячие точки аллокаций проступают чётко. На выходе — статистически значимая картина среднего потребления памяти. Ровно это нам и нужно.

С помощью jemalloc мы снимаем такой дамп раз в 12 часов, а также за долю секунды до того, как происходит process suicide — самозавершение процесса при достижении лимита памяти, чтобы избежать «убийства» случайных процессов OOM Killer. Дамп сжимается и уходит в телеметрию; на самой колонке он никак не обрабатывается — только собирается и отправляется.

Есть нюанс, про который нельзя забывать: пока jemalloc делает дамп, все аллокации в процессе блокируются. У нас эта пауза — 3 мс в среднем и 25 мс на 99,9-м перцентиле (по всем моделям). Цифры небольшие, потому что и дампы небольшие из‑за нашего параметра семплирования в 1 МБ. Для пользователя такое подвисание незаметно.

Агрегация дампов — сбор статистики по группам

Каждый день колонки присылают порядка 30 миллионов дампов. Смотреть на отдельный дамп почти бесполезно — в нём всего горстка случайных семплов. Поэтому единичные дампы мы объединяем в кластеры. Это даёт, во‑первых, статистически значимое среднее потребление памяти внутри каждого кластера, а во‑вторых — возможность осмысленно сравнивать потребление между группами устройств.

Весь флот мы нарезаем на кластеры по нескольким признакам:

  • Модель устройства и версия прошивки. Все дампы с одной версии можно символизировать вместе — исполняемые файлы у них одинаковые.

  • Процесс. Мы умеем профилировать память в разных процессах, но в этой статье говорим только про главный — maind.

  • Группа. Колонки поделены на группы: production — дефолтная для всех продовых устройств; beta — участники бета‑тестирования; release_test и release_baseline — две специальные группы, в которые случайно набирается примерно по 10% устройств каждой модели. Они нужны для A/B‑тестирования прошивок: на тестовую группу катится новая версия, на baseline остаётся текущая, после чего две группы сравниваются по метрикам — например, не выросло ли потребление памяти в новой версии.

  • Потребление RSS. Берём не больше 1 млн дампов на кластер (с максимальным RSS), а внутри этого среза дополнительно выделяем подкластеры. Например, топ-50 000 (примерно 5%), топ-10 000 (примерно 1%) и топ-2000 (примерно 0,2%) дампов по RSS.

  • Дампы перед process suicide собираем в отдельный кластер.

Как идёт агрегация и символизация

Раз в сутки мы прогоняем все семплированные дампы за предыдущий день — те самые ~30 млн записей, которые лежат в нашем YTsaurus‑кластере. Пайплайн такой:

  • SQL‑запросом в YTsaurus режем дампы на кластеры, внутри каждого сортируем по RSS и оставляем не больше 1 млн, затем группируем по версии прошивки. После фильтрации остаётся примерно 17 млн дампов.

  • Дальше работаем в рамках одной версии прошивки: загружаем её дебаг‑символы, сливаем дампы каждого кластера в один «усреднённый» дамп и символизируем результат.

  • Готовые покластерные дампы складываем в базу данных YDB — для визуализации и сравнения.

Агрегация в деталях: цифры и факты

Сама агрегация написана на Python, а символизацию делает addr2line, причём каждый уникальный адрес символизируется ровно один раз. Любопытно, что Python здесь не узкое место: 65% CPU съедает addr2line, ещё 20% уходит на разжатие и парсинг дампов и 10% — на слияние. Эти доли, кстати, мы померили с помощью Perforator.

Считается всё параллельно — через multiprocessing.Pool с распараллеливанием по данным, то есть по моделям устройств. Целиком прогон занимает около 3 часов на 10 ядрах CPU и ~32 ГБ памяти, плюс ещё 30–60 минут на SQL‑запрос в YTsaurus. За счёт группировки по версии дебаг‑символы (мы храним их в CI) скачиваются ровно один раз на версию, а сами дампы подгружаются из YTsaurus «лениво» — по мере парсинга и слияния.

Главная загвоздка при слиянии связана с ASLR (address space layout randomization): из‑за рандомизации адресного пространства адреса в разных дампах не совпадают, поэтому сначала их нужно привести к единой системе координат. После этого остаётся объединить и усреднить статистику по стекам. Например, при слиянии двух дампов:

  • если стек А есть в обоих со взвешенным размером 1 МБ — в среднем в стеке А выделен 1 МБ;

  • если стек Б со взвешенным размером 1 МБ встречается только в одном из двух дампов — в среднем по стеку Б выделено 0,5 МБ.

Чтобы не раздувать память, сливаем иерархически: сначала дампы группами по 1000 сливаются в промежуточные, а уже промежуточные — в один финальный. И тут срабатывает приятный побочный эффект: дампы отсортированы по RSS, поэтому подкластеры топ‑N (топ-50 000, топ-10 000 и так далее) достаются почти бесплатно — достаточно слить только первые промежуточные дампы. 

На выходе из миллиона дампов получается порядка 10–40 тысяч уникальных стеков (зависит от модели и общего потребления памяти). Символизируем уже после слияния — с адресами там работать проще и быстрее.

И про фильтрацию: из 30 млн дампов в день мы реально обрабатываем около 17 млн. Основная причина — тот самый кап в 1 млн дампов на большой кластер. Ещё 20–40 тысяч дампов отбрасываем как совсем мелкие кластеры — nightly‑сборки и прочие редкие версии и группы.

Внутренний инструмент для анализа потребления памяти

Чтобы работать с объединёнными дампами, мы сделали внутренний веб‑сервис — он умеет искать, визуализировать и сравнивать дампы.

Поиск дампов

На главной странице показаны дампы за день для главного процесса production‑групп yandexmicro (это внутреннее название для Станции Лайт). Видно несколько кластеров.

production_1000k — кластер из миллиона дампов с самым высоким потреблением по RSS — примерно топ-40%. В колонке Stats у него такие средние значения:

  • 32,9 МБ — выделено через вызовы malloc; именно эти аллокации и отслеживает дамп.

  • 24,0 МБ — RSS anon. Может быть меньше, чем аллоцировано: не все аллоцированные страницы реально трогаются и занимают память.

  • 32,3 МБ — RSS file. Растёт по мере того, как в память подгружаются куски исполняемого кода и других файлов.

  • 13 часов — среднее время жизни процесса. Здесь оно ожидаемо: production yandexmicro мы обновили в ночь на 20 мая, так что к моменту снятия дампов процессы прожили примерно столько.

Production_50k/production_10k/production_2k — топ-50 000, топ-10 000 и топ-2000 дампов по RSS. По колонке Stats видно, что в этих кластерах среднее потребление выше по всем трём показателям — и по аллокациям, и по RSS anon, и по RSS file. Что логично: это агрегация дампов с колонок, которые едят больше всего памяти.

Production suicide — в последней строке собраны дампы, снятые перед process suicide в группе yandexmicro. Таких дампов всего 105, и среднее потребление у них ожидаемо большое — 60,7 МБ по аллокациям. Это дампы, где память у главного процесса вышла за лимит.

Визуализация дампов

Есть несколько способов визуализировать дамп. Один из них — Flame Graph, популярный набор скриптов для отрисовки профилей CPU и памяти. Чтобы flamegraph.pl (и соседние скрипты) заработал, дамп достаточно представить в виде стеков с размерами. Смотреть полезно в обе стороны: и от корня до malloc (как на картинке), и в обратном порядке — например, чтобы увидеть все стеки, которые приходят в конструктор std::string.

Вот пример с выделенным стеком от YandexMicroLedsInstaller: видно, что анимации аллоцируют 114 КБ динамической памяти.

Второй способ, которым мы пользуемся, — pprof. Конвертируем дампы в нужный формат и запускаем pprof прямо на нашем сервере. 

Здесь, например, выделен стек, в котором инициализируется VQE (тот самый компонент обработки звука, благодаря которому Алиса лучше «слышит»), — он потребляет почти 3 МБ.

Сравнение дампов

Смотреть дампы полезно, но ещё полезнее их сравнивать. Можно делать это глазами — вдумчиво вглядываться в два Flame Graph. Но лучше взять инструмент: скрипт difffolded, тот же pprof (он умеет диффать два дампа) или наше простое текстовое сравнение, которое мы сделали, чтобы быстро замечать разницу.

Вот, например, первые строки сравнения потребления между production_2k и production_1000k. Видно, что главный прирост (почти на 1 МБ в среднем) приходится на BioCapability — метод, который распознаёт голос пользователя во время запроса. Это логично: колонка ест больше, если пользователь «познакомил» её со своим голосом (настроил узнавание) и прямо сейчас идёт запрос. И логично, что в верхних перцентилях по памяти такие стеки встречаются в среднем чаще.

Сравнение дампов закрывает сразу много задач: можно разобраться, почему случается process suicide, куда уходит память в верхних перцентилях, чем различаются группы или даже сколько памяти будет «стоить» конкретный флаг — последнее проверяется A/B‑экспериментом.

Но чаще всего мы используем сравнение для релизов. Каждую новую версию прошивки мы сравниваем с предыдущей по потреблению памяти и сразу видим, где стало лучше, а где хуже. Причём для релизов это работает автоматически: каждый день сервис сравнивает release_baseline (текущую версию) и release_test (новую) по всем моделям колонок, собирает отчёт с пометками о важных изменениях и кидает его в чат. Если в каком‑то стеке потребление подскочило — найти виновный коммит и принять меры уже несложно.

А вот как это выглядело раньше: при росте RSS в новой версии приходилось перебирать коммиты за неделю и гадать, какой из них виноват. Собственно, ровно ради того, чтобы перестать гадать и начать «видеть» память, всё и затевалось.

Особенности сравнения разных версий

У сравнения версий дампов есть неприятный нюанс: один и тот же стек в двух версиях может выглядеть по‑разному. Адреса, понятно, разъедутся — но это не беда, мы ведь уже работаем с символизированными именами. Хуже, что между версиями меняются номера строк, имена функций и вообще структура кода. Из‑за этого «одна и та же» точка выделения памяти в двух версиях может не схлопнуться в одну строку, и сравнение покажет ложное изменение — будто в одном стеке аллокации добавились, а в другом исчезли.

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

Примеры найденных проблем

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

Поиск и исправление утечек памяти

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

Одна из недавних находок — утечка памяти в BioCapability. Проблема была редкая: повторялась всего 10–30 раз в день и только на Станции Миди и Станции 3 (суммарно где‑то 1–2 млн устройств в проде).

Искали так. Сначала посмотрели на дамп по process suicide из production‑группы Станции Миди — таких событий там порядка 50 в день. Данные усреднённые, поэтому в дампе нашлось несколько ожидаемо «жирных» стеков, но среди типичных мелькнул один странный — из BioCapability.

Стек вёл в сценарий, где колонка распознаёт голос человека прямо во время запроса: звук с микрофонов мы передаём в локальную модель биометрии. И выглядело всё так, будто звук в неё поступает бесконечно, пока не закончится память.

Чтобы понять, в чём дело, спустились на уровень отдельных семплированных дампов. Из тех же 50 нашлись пара десятков, где причиной process suicide был именно этот стек BioCapability. Эти единичные случаи мы вручную разобрали по телеметрии.

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

После раскатки этот стек полностью исчез из причин process suicide на Станции Миди и Станции 3, а общее число process suicide на этих моделях заметно снизилось. Без инструмента мы такую проблему, скорее всего, просто не нашли бы: она редкая и почти не воспроизводится локально.

Оптимизации

Методом пристального вглядывания в Flame Graph можно нащупать и места для оптимизации. Так, например, мы в несколько раз ужали анимации часов на наших устройствах и сэкономили порядка 700 КБ памяти.

Небольшой контекст. На колонках есть несколько видов светодиодных сборок:

  • Часы — например, как на Станции Лайт 2. Кроме времени, умеют показывать погоду, анимацию «размышления» и так далее.

  • «Окникс» — значок Алисы сверху: светится фирменным фиолетовым, крутится, когда колонка думает, мигает зелёным при уведомлении.

  • Кольцо — круг из светодиодов сверху (например, на Станции Миди и Станции Мини 3 Про). Проигрывает анимации во время общения, воспроизведения музыки, а ещё умеет работать как ночник или лава‑лампа.

Однажды мы смотрели дамп на Станции Мини 2 и заметили странность: анимации часов (ScLedStorage) занимают в несколько раз больше динамической памяти, чем анимации «окникса» — хотя последних больше и они сложнее.

Полезли в код и дамп — и увидели, что кадры анимаций часов хранятся крайне неэффективно. По сути, каждый кадр — это 32 числа: по значению яркости на каждый светодиод, плюс длительность. Но вместо простого вектора яркости использовался std::map, да ещё и с нетривиальным классом ScLedLocation в роли ключа. Так было написано для удобства работы с анимациями. Беда в том, что на каждый кадр такой map разворачивает целое дерево узлов с объектами‑ключами — и эти 32 значения внезапно обходятся в десятки аллокаций.

После рефакторинга мы заменили это:

std::map<ScLedLocation, uint8_t> elements;

На это:

std::vector<uint8_t> elements;

Кадр анимации стал выглядеть так:

struct ScLedFrame {
    std::vector<uint8_t> elements;
    std::chrono::milliseconds delayMs{0};
};

В следующем релизе результат виден сразу.

Потребление в стеке анимаций упало с 750 КБ до 40 КБ. Это «бесплатные» 0,7 МБ памяти, в случае Станции Мини 2 крайне драгоценной.

Анализ экспериментов

Ещё одно применение профилирования — анализ потребления RSS в экспериментах.

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

Сам эксперимент удался: мы одновременно снизили число ошибок воспроизведения (понижая качество тем, у кого они были) и подняли средний битрейт (повышая качество тем, у кого соединение стабильное). Но у группы с автокачеством подросло и среднее потребление памяти.

Веб‑сервис показал, в каких стеках случился рост, — это оказались стеки нашего аудиоплеера. Вместе со средним битрейтом выросло потребление под буферы HTTP‑клиента, который подгружает музыкальный файл прямо во время воспроизведения. В этом эксперименте такой рост ожидаем — но важно было убедиться, что память выросла именно здесь, а не, скажем, из‑за неосвобождённых ресурсов при динамической смене качества. Инструмент дал это проверить, а не гадать.

Заключение

Этот инструмент мы запустили в прошлом году. Он не решает всех проблем с памятью — в конце концов, он показывает лишь точку аллокации, но не то, кто эту память держит и почему её не освобождает. И всё же даже этого хватило, чтобы кардинально изменить нашу работу: упростить выкатку новых версий и поиск как утечек, так и мест для оптимизации. Мы перестали играть в угадайку при росте потребления и теперь видим вклад каждого компонента.

Главный вывод простой: профилировать C++ в проде на миллионах устройств — реально, полезно и не требует заметных ресурсов. А мы планируем развивать инструмент и дальше.

Комментарии (0)