Xamarin и Garbage Collector на Андроид

При тестировании Андроид-версии нашего мобильного приложения, мы неожиданно начали замечать ощутимые проблемы в производительности — время от времени (достаточно регулярно) приложение «подвисало» на несколько секунд, а в некоторых случаях и вовсе вызывало ANR (Application Not Responding — такое замечательное окошечко, которое говорит, что приложение не отвечает и предлагает его закрыть).

Гугл подобных ситуаций дает подробные рассказы о проблемах с Garbage Collector’ами, и даже о том, что все языки с GC прокляты :) А сама проблема, обсуждать которую мы начали на форуме Xamarin’a, проявлялась в довольно странном факте:

Как все знают у сборщиков мусора есть два основных режима сборок:

  • минорная (GC_MINOR), которая анализирует и уничтожает недавно созданные объекты
  • мажорная (GC_MAJOR), которая проходит по объектам более старших поколений, и пытается собрать их

По определению, минорные сборки должны проходить относительно часто и быстро, а мажорные — реже и занимать больше времени. В нашем же случае, по прошествии весьма короткого времени с запуска приложения, Минорные сборки начинали занимать по 2-3 секунды, а мажорные — порядка 0.5с. Конечно, stop the world на пару секунд невозможно не заметить! Оказывалось, что минорная сборка анализирует порядка 600К объектов — весь тот кэш приложения, который находился в оперативной памяти.

Процесс локализации ошибки (а точнее, минимального набора действий, который приводил к такому поведению Garbage Collector’a) был весьма нетривиален, но был-таки успешно сведен к достаточно небольшому приложению: а именно, выполнению 5 и более параллельных https запросов (последовательное выполнение запросов или использование http к подобному поведению не приводили).

В итоге, благодаря поддержке Xamarin’a мы применили workaround для проблемы — корень большого графа из порядка 570K объектов (которые необходимо хранить в памяти), был сделан статичным, что решило проблему (GC_MINOR стал анализировать порядка 20K объектов, что намного лучше и весьма быстро). Для противников использования static’ов в бизнес-логике, корни графов можно помещать в отдельный статичный список, созданный специально для целей оптимизации GC.
В этом случае не пострадает чистота кода (нет принуждения к использованию static), и ярко выраженных проблем со сборщиком мусора не будет.

P.S. Конечно, подобное «решение» проблемы — это именно workaround. Надеюсь, что поведение GC в этом плане будет в ближайшее время улучшаться.

P.P.S. Вторую часть проблемы — что из вышеупомянутых 5 параллельных запросов успешно завершаются всегда только 2, а остальные повисают в «неопределенном» состоянии (не падают с исключением, но и не завершаются с каким-либо кодом) мы продолжаем плодотворно обсуджать с техподдержкой.

P.P.P.S. В интернете многое пишут о том, что GC становится узким местом для real-time приложений какого-либо рода. Возможно, это действительно применимо к приложениям, активно работающим с графикой или использующими ресурсы процессора «на полную мощность», но для среднестатистических бизнес и даже enterprise приложений, в том числе и достаточно больших, GC серьезных проблем не представляет. И для подобных приложений я могу однозначно рекомендовать использование Xamarin.Android.

Опубликовать в Facebook
Опубликовать в Google Plus

2 комментария

  1. Спасибо большое за статью, с интересом прочитал.
    Можете ли более подробно пояснить этот момент «Для противников использования static’ов в бизнес-логике, корни графов можно помещать в отдельный статичный список, созданный специально для целей оптимизации GC.»? Есть ли у вас пример кода, который покажет как данную идею можно реализовать?

  2. на примере тестового приложения, которое прикрепено к статье, проблему можно решить двумя способами:
    1) сделать «тяжелое» поле статичным, исправив 19 строку в MainActivity.cs на:
    static Cache _cache;

    2) Если по каким-то соображениям не хочется делать поля в бизнес-логике/UI статичными, можно помещать все «тяжелые» объекты в синглтон-хранилище. Например, добавив что-то подобное в конструктор класса Cache:

    public class Cache {
    public Cache() {
    HeavyObjectsStorage.Storage.Add(this);
    }
    }
    public static class HeavyObjectsStorage {
    public static ListStorage = new List();
    }

    Первый способ очевиднее и проще, второй может быть оправдан в больших проектах. Оба «решают» проблему одинаково.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *