Комментарии 15
Отличный ресерч!
Единственное, немного смущает формулировка: «Однако на самом деле с IO запрос выполнился быстрее в 10.92 раз!»
Ведь время выполнения самого сетевого запроса не сильно изменилось. Изменилось лишь время от запуска корутины до следующего действия на ui (hideKeyboard())
А вы случайно не измеряете время создания экземпляра Retrofit и вашего ApiService? Функция provideApiService() уж очень подозрительно выглядит, и конечно, если на каждый запрос создавать новый инстанс ApiService, будет медленно.
Другими словами, удалось ли понять, за счёт чего именно вы добились ускорения? Шаги "заметить проблему" и "решить проблему" были, а промежуточного "понять, где тормоза" я не заметил :)
Конечно, на реальных проектах я не создаю новый объект ApiService на каждый запрос.
Здесь для демо-проекта добавила такую реализацию, т.к. задача была в том числе показать, что есть проблемы со скоростью первого запроса.
Описанное поведение с задержкой было замечено именно на боевом проекте в первую очередь
Тогда расскажите, что именно было медленным, какая именно часть кода внутри вызова provideApiService().getSomethingFromApi()
исполнялась на главном потоке?
В статье об этом не написала, но к счастью в комментариях ниже gmk57 здорово раскрыл эту тему
Спасибо за любопытную и немного провокационную статью. :)
Время в 500+ миллисекунд меня сильно удивило, да честно говоря и 50 мс после исправления - это неприлично много.
Очень здорово, что вы выложили тестовый проект. Без него вряд ли удалось бы воспроизвести результаты. Погонял его на стареньком (6 лет, Snapdragon 410) аппарате, и вот что получилось (время в формате "первый запуск / последующие запуски"):
getMainThreadResult 620 / 18 мс
getRetrofitFixResult 57 / 8 мс
getWithContextResult 12 / 2 мс
Основную часть времени занимает создание reflection-based JSON-адаптера:
Замена его на kotlin-codegen дает ускорение в разы:
getMainThreadResult 92 / 11 мс
getRetrofitFixResult 63 / 8 мс (тут нет ускорения, разрыв сильно сокращается)
Но в принципе вызывать Retrofit.Builder()....create(ApiService::class.java) каждый раз по нажатию кнопки - сомнительная идея, обычно это делается единожды при старте приложения.
Такой рефакторинг ускоряет getMainThreadResult еще в разы: 25 / 3 мс
Из этих 25 мс основная часть уходит на создание CatFactResponseJsonAdapter (один раз при первом вызове любого метода, возвращающего CatFactResponse).
3 мс - это главное время, от которого имеет смысл отталкиваться. getRetrofitFixResult и getWithContextResult по-прежнему чуть быстрее (1-3 мс), но стоит ли это мучений с обертками над обертками? ;) Простые архитектурные изменения (см. выше) дают гораздо больший эффект.
Цифры выше можно рассматривать как worst-case scenario: старый аппарат, debug build без AOT-компиляции и прочих рекомендуемых хитростей.
Мораль: профайлер - наше всё. ;)
Большое спасибо за такой анализ!)
Замечания справедливые, но к сожалению не у всех есть кодген на проектах. И даже знаю пару разрабов, которые кодогенерацию не любят) Насчет вытеснения инстанса ApiService: это не решает проблему замедления первого запроса
Сочувствую. Reflection - это боль и по производительности, и в сочетании с R8, а kotlin-reflect еще и здоровенная либа в рантайме.
Вы пробовали закинуть issue в Retrofit? Сходу не нашел. Логично было бы создавать адаптеры в Dispatchers.IO, но возможно у них были причины так не делать.
Применил решение описанное в статье в рабочем коде и в сложной цепочке flatMapMerge()... .toList() получаю бесконечное ожидание, меняю решение из статьи на обычный заворот в корутин диспатчер, все ок.
Не рассматривали вариант просто вернуться к адаптору от Вортана?
После сборки — доработать напильником. Фиксим Retrofit для Корутин