Яндекс Карты — это не просто приложение для навигации, а один из самых высоконагруженных мобильных сервисов с широкой аудиторией. В центре всех наших сценариев — сама карта, которая уже сегодня не уступает по сложности отдельным игровым движкам. Мы постоянно работаем над тем, чтобы сделать её более удобной для ориентирования, и постепенно идём к высокодетализированным и реалистичным картам будущего: добавляем трёхмерные здания, разметку на дорогах, детализированные развязки и другие городские объекты. 

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

Меня зовут Игорь Зверев, я руководитель группы разработки автонавигации в Яндекс Картах. Сегодня я расскажу, как мы подошли к решению этой задачи: что изменили в процессе разработки, как создали и используем систему RAM‑классов для выпуска требовательных функций и какие технические выводы сделали на этом пути.


С чего всё началось

Как и везде в Яндексе, перед включением новой функциональности на всех пользователей мы проводим множество раундов A/B‑тестирования. Но в последнее время многие наши эксперименты начали завершаться неудачно: сильно росло количество аварийных завершений работы приложения (крэшей). Также мы заметили, что общий фон успешных сессий взаимодействия с приложением медленно, но неуклонно снижался.

На тот момент в A/B‑экспериментах мы могли видеть только одну общую метрику: суммарное количество крэшей. Из одного только этого числа мы не можем сделать вывод, что же всё‑таки произошло: проблема в самой функциональности, в интеграции с другими частями нашего приложения или, может быть, в чём‑нибудь ещё. 

Чтобы разобраться с результатами каждого такого эксперимента, приходилось отсматривать «глазами» крэши, случившиеся в контрольной и тестовой выборках, и искать различия. В рамках таких разборов мы заметили, что всё в большем числе экспериментов метрика количества крэшей растёт не из‑за ошибок в тестируемой функциональности, а из‑за проблем, связанных с нехваткой оперативной памяти: OOM.

Что такое out of memory (ООМ)

Эта ошибка возникает, когда приложение пытается запросить дополнительное пространство в оперативной памяти устройства, но операционная система не выделяет запрашиваемый ресурс. Причин, по которым система принимает такое решение, несколько, но мы не будем сильно на этом останавливаться (подробнее об этом — в докладе нашего коллеги Жени Васильева «Память в Android, утечки и OOM„).“»

Первой естественной реакцией на появление подобных проблем всегда является желание попрофилировать и пооптимизировать текущее поведение — и именно так мы и сделали. Мы провели множество раундов оптимизаций и поисков утечек памяти во всех частях нашего приложения, как в нативном коде на C++, так и на обеих наших платформах: iOS и Android. В результате этой работы мы смогли увидеть на наших дашбордах вот такую приятную картину:

На графике выше отражены две главные метрики стабильности нашего приложения: Crash‑Free‑пользователи (процент пользователей, которые не столкнулись с крэшами) и Crash‑Free‑сессии (процент сессий, завершившихся без ошибок)

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

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

  • Что нам делать, чтобы избежать подобных проблем? То есть достаточно ли нам просто наличия регулярного процесса профилирования и оптимизаций?

Гипотезы

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

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

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

Гипотеза 1: чем меньше памяти — тем выше вероятность крэша

Для демонстрации правдивости первого предположения обратимся к графику ниже:

Данные собраны за месяц. На графике:

  • По горизонтальной оси — категории устройств, сгруппированные по количеству доступной оперативной памяти (от меньшего объёма к большему).

  • Синие столбцы — количество сессий пользователей на устройствах с определённым количеством памяти.

  • Жёлтая линия — суммарное количество сессий, в которых случился крэш, связанный с недостатком памяти.

  • Красные вертикальные линии — разделение девайсов на когорты по их железу (мы вернёмся к этому немного позже).

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

Вот тут и появляются те самые экстраординарные результаты, о которых я писал выше: в нашем случае 9% сессий в левой части графика (до первой красной линии) «генерировали» до 96% всех ООМ. Мы предполагали, что, возможно, получим похожий результат, но такой ярко выраженной концентрации не ожидал никто.

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

Гипотеза 2: нельзя превышать предел потребления памяти. Но какой?

Как было сказано выше, в отличие от первой гипотезы, вторая представляла для нас ещё больший интерес. Давайте посмотрим на график, который подтверждает и её:

На графике выше:

  • Каждая точка — один день из 2024 года.

  • По горизонтальной оси — 99-й перцентиль потребления оперативной памяти по всем пользователям, которые заходили в приложение в этот день.

  • По вертикальной оси — процент сессий, который не завершился проблемой с нехваткой оперативной памяти.

Заметно, что у графика есть несколько частей:

  • В левой и средней части мы видим некоторое снижение и выход на плато хороших сессий.

  • В правой же части графика после некоторого предела (как раз этот предел мы и искали в рамках второй гипотезы) заметно резкое снижение числа успешных сессий.

Промежуточные выводы

Проведя исследования по гипотезам выше, мы смогли также ответить на интересующие нас вопросы:

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

Ответ: мы дошли до предела потребления ресурсов примерно на 10% девайсов в аудитории нашего приложения. Преимущественно это устройства с 2–3 ГБ оперативной памяти. При этом важно отметить, что на медианном устройстве в нашей аудитории уже сейчас установлено от 5,5 до 7 ГБ оперативной памяти.

Достаточно ли нам текущих оптимизаций?

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

Стоит также отметить, что упомянутый во второй гипотезе предел был превышен только на самых слабых устройствах. На устройствах с бо́льшим объёмом оперативной памяти значимых изменений в поведении не наблюдалось. Важный момент тут заключается в том, что нет какого‑то единого предела для всех устройств: у самых слабых устройств он один, у более сильных — другой.

Идея, как всё исправить

В чём же заключалась идея, которая формировалась у нас в рамках наших изысканий?

Наше приложение работает на широком спектре различных девайсов, и мы находимся в интересной ситуации: 

  • на устройствах с небольшим объёмом оперативной памяти мы уже страдаем;

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

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

В процессе анализа работы нашего приложения мы пришли к выводу, что необходимо разделить устройства пользователей на несколько классов в зависимости от объёма оперативной памяти (RAM‑классов) и иметь возможность выпускать RAM‑ёмкую функциональность только на их часть.

Деление на RAM-классы: причины и нюансы решения

Почему именно RAM‑классы? В рамках наших изысканий мы проверили множество гипотез, в том числе зависимость объёма потребляемой памяти и количества ООМ от размеров экрана устройства в дополнение к RAM‑классам. Идея родилась из того, что достаточно важная часть нашего приложения — это карта, которая в последние годы становится всё детальнее: мы добавляем дороги с разметкой как в городе, 3D‑модели зданий, развязок и других элементов городской инфраструктуры. И чем больше экран, тем больше контента на нём будет отображаться — соответственно, потребление памяти и вероятность ООМ будут возрастать.

Но оказалось, что в нашей текущей ситуации добавление ещё одной оси классификации всё только усложнит. Продемонстрировать это можно с помощью такого графика:

Здесь представлена матрица корреляции между параметрами экрана устройства:

  • площадь экрана в дюймах (area_in_inches);

  • площадь экрана в пикселях (area_in_pixels);

  • плотность пикселей на экране (density per inch — DPI).

Про потребление различных типов памяти нашим приложением — столбцы pss (Proportional Set Size), rss (Resident Set Size), графическая память (graphics) и другие — более подробно можно узнать в докладе нашего коллеги «Память в Android, утечки и OOM„, который уже упоминался выше.“»

В каждой ячейке графика расположено число от –1 до +1 — это коэффициент корреляции. Можно заметить, что параметры экрана либо совсем не скоррелированы с потреблением памяти, либо совсем немного.

Ещё одна, возможно, более визуально приятная картинка, подтверждающая утверждение выше

На графиках выше каждая строчка — это децильная группа по объёму установленной памяти на устройстве, а каждый столбец — это разбиение на пять равных групп по DPI экрана. Синие «коробочки» — потребление оперативной памяти во время обычной работы приложения. Оранжевые «коробочки» — объём аллоцированной памяти в момент ошибки out of memory. Ожидания были такие, что в каждой строке синие коробочки от столбца к столбцу должны располагаться всё выше, но это оказалось не так.

Таким образом, мы решили остановиться только на разделении пользователей по RAM‑классам.

Наши требования к разделению:

  • Количество RAM‑классов должно быть ограниченным, чтобы упростить анализ и поддержку, но в то же время достаточным для возможности продолжительное время выпускать новую функциональность, постепенно отсекая нижние RAM‑классы.

  • Внутри одного RAM‑класса должны быть устройства с примерно одинаковым объёмом памяти. Это позволит оценивать предел потребления памяти в рамках группы похожих устройств.

  • Необходимо иметь возможность изменять набор доступной функциональности в зависимости от RAM‑класса устройства как в обычных условиях, так и в экспериментах.

  • Приложение должно самостоятельно определять, к какому RAM‑классу относится устройство.

Почему приложение должно само определять свой RAM‑класс? Самая важная причина заключается в том, что нам нужно адаптировать функциональность приложения уже при первом его запуске. Можно загружать конфиг RAM‑классов из сети, но тогда нам придётся делать сложный выбор, пока конфиг не пришёл: какую функциональность включать, а какую — нет. То есть мы либо выключим лишнее, либо не включим нужное.

Сложности A/B-экспериментов

Перед нами стоял вопрос: как правильно проводить A/B‑эксперименты с ресурсозатратными фичами, чтобы не ухудшать пользовательский опыт на слабых устройствах?

Есть мысль: почему бы просто при проведении эксперимента не передавать одним из экспериментальных параметров минимальный RAM‑класс, чтобы приложение просто не включало функциональность, если есть несоответствие?

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

  1. Когда мы хотим запустить эксперимент, мы идём в сервис (который называем АБ‑шницей) и заводим там эксперимент.

  2. Приложение ходит в сеть и получает от бэкенда АБ‑шницы набор экспериментальных параметров, далее в зависимости от эксперимента они применяются либо моментально, либо на следующий запуск.

  3. Приложение работает, мы собираем логи и отправляем их в AppMetrica.

  4. Логи агрегируются, считаются нужные метрики, оценивается статзначимость изменений.

  5. Все эти данные доезжают до АБ‑шницы, где мы уже и видим нужную нам статистику.

Так в чём же проблема?

А вот в чём: если минимальный RAM‑класс — это один из параметров эксперимента, то ни у агрегатора логов, ни у самой АБ‑шницы не будет возможности отличить, заработала всё‑таки функциональность или нет. Для них это выглядит так, что приложение эксперимент получило и применило, следовательно, функциональность работает.

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

Понятно, что вопросы выше можно решить (например, ручным пересчётом метрик), но есть решение намного лучше: сделать так, чтобы эксперимент просто не «доезжал» на неподходящие устройства. Мы так и сделали: теперь, когда мы идём в сеть, чтобы получить конфиг экспериментов, то мы также передаём и RAM‑класс устройства. Это позволяет просто модифицировать конфиг экспериментов так, чтобы в него не попадали неподходящие устройства.

Итог

Итак, для улучшения работы приложения Карт мы разделили устройства на пять RAM‑классов для каждой платформы (iOS и Android). Это количество позволяет довольно точно классифицировать устройства, оценивать их возможности и не думать о повторении ситуации, когда мы не можем выпускать новую функциональность.

Для более точного анализа работы приложения мы расширили набор метрик стабильности. Теперь мы отслеживаем количество ООМ‑ошибок, крэшей, а также объём потребления оперативной памяти не только в общем по приложению, но и в рамках каждой платформы и каждого RAM‑класса. Это позволяет нам принимать взвешенные решения о том, какая функциональность должна быть включена по дефолту и на каких устройствах.

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

Благодаря этому подходу нам удалось запустить новый дизайн Карт с детализированными дорогами с разметкой на всех пользователей. А ещё 90% пользователей получили новые 3D‑развязки в автомобильной навигации по дефолту. 

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


  1. nerudo
    30.10.2025 07:42

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


  1. strelkove
    30.10.2025 07:42

    Яндекс Карты — это не просто приложение для навигации

    Простите, не удержался


  1. porok
    30.10.2025 07:42

    А можно всё таки сделать отдельное приложение карт без всех этих 3д развязок и отличных предложений? Или добавьте переключатель - чтобы можно было отключить все непрофильные плюшки.

    Я готов на всех своих устройствах страдать с функционалом "ограниченным" лишь картами.


    1. nerudo
      30.10.2025 07:42

      Нужно всего лишь зарезать приложению память до 512МБ. Правда не исключено, что останутся только предложения =)


  1. id_Alex
    30.10.2025 07:42

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

    А как пользователь поймёт что у него достаточно мощное устройство и некая новая экспериментальная функциональность включилась и работает?


    1. Hlad
      30.10.2025 07:42

      Гораздо интереснее, как это ощутят покупатели китайских "Самсунгов с 16 гигами оперативы всего за 5 тысяч рублей". Откуда Яндекс берёт информацию о ресурсах платформы?


  1. Jijiki
    30.10.2025 07:42

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

    наверно для широты тестирования надо помимо простого прохода теста еще прокопировать теже тесты и врубать приложение на 5 суток


  1. skhida
    30.10.2025 07:42

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