Профилирование Kotlin кода: находим узкие места

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему профилирование Kotlin кода критически важно

При разработке на Kotlin — современном, лаконичном и безопасном языке для JVM, Android и мультиплатформенных проектов — производительность часто остаётся в тени. Мы пишем чистый код, используем корутины, делегаты и расширения, но однажды приложение начинает тормозить: анимации подвисают, API-запросы обрабатываются дольше ожидаемого, а память растёт как на дрожжах. В таких случаях интуиция и код-ревью не помогут — нужен научный подход.

Профилирование — это процесс измерения времени выполнения, использования памяти, загрузки ЦП и других метрик для выявления узких мест. Без него вы рискуете оптимизировать не те участки, тратя часы на бесполезные микрооптимизации. В этой статье мы разберём, как профилировать Kotlin код, какие инструменты использовать и как интерпретировать результаты. Материал будет полезен Android-разработчикам, бэкенд-инженерам на Ktor и всем, кто пишет на Kotlin для JVM.

Основные метрики и инструменты профилирования

Прежде чем углубляться в код, важно понимать, что именно мы измеряем. Основные метрики профилирования:

  • Время выполнения (CPU Time) — сколько процессорного времени занял метод или блок кода.
  • Wall Clock Time — реальное время от начала до конца выполнения, включая ожидание ввода-вывода.
  • Использование памяти (Heap Memory) — количество объектов, созданных в куче, и время сборки мусора (GC).
  • Количество аллокаций — сколько раз был создан новый объект. Часто является скрытым убийцей производительности.

Для Kotlin на JVM доступны следующие инструменты:

  • IntelliJ Profiler (Async Profiler) — встроенный в IntelliJ IDEA Ultimate профилировщик для CPU и памяти. Поддерживает sampling (выборку) и tracing (трассировку).
  • JProfiler — мощный коммерческий инструмент с визуализацией потоков, кучи и GC.
  • VisualVM — бесплатный профилировщик, входящий в состав JDK. Позволяет мониторить работающее приложение.
  • YourKit — ещё один коммерческий профилировщик с отличной поддержкой Kotlin и корутин.
  • Android Studio Profiler — для Android-приложений: CPU, Memory, Network и Energy.

Для микробенчмаркинга (измерения производительности отдельных функций) используйте kotlinx-benchmark — библиотеку на основе JMH (Java Microbenchmark Harness).

Практический пример: профилирование CPU и памяти

Шаг 1: Подготовка кода для профилирования

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

data class User(val id: Int, val name: String, val email: String)

fun generateReport(users: List<User>): String {
    val sb = StringBuilder()
    for (user in users) {
        sb.append("ID: ${user.id}, Name: ${user.name}, Email: ${user.email}\n")
    }
    // Дополнительная обработка: фильтрация и сортировка
    val filtered = users.filter { it.email.contains("@") }
        .sortedBy { it.name }
    for (u in filtered) {
        sb.append("Filtered: ${u.name}\n")
    }
    return sb.toString()
}

Кажется безобидно, но при списке в 100 000 элементов это может стать проблемой.

Шаг 2: Запуск CPU профилирования в IntelliJ IDEA

Откройте IntelliJ IDEA Ultimate, запустите приложение и выберите Run → Profile 'main' (или используйте иконку профилирования рядом с кнопкой Run). IntelliJ запустит Async Profiler и покажет Flame Graph (пламенный граф).

После завершения профилирования вы увидите что-то вроде:

CPU profiling result:
- generateReport: 85% времени
  - String concatenation (sb.append): 45%
  - filter + sortedBy: 30%
  - StringBuilder constructor: 10%

Узкое место очевидно: filter и sortedBy создают промежуточные коллекции, а конкатенация строк через + внутри ${} порождает множество объектов StringBuilder.

Шаг 3: Оптимизация и повторное профилирование

Перепишем код с учётом узких мест:

fun generateReportOptimized(users: List<User>): String {
    val sb = StringBuilder()
    for (user in users) {
        sb.append("ID: ").append(user.id)
            .append(", Name: ").append(user.name)
            .append(", Email: ").append(user.email)
            .append('\n')
    }
    // Избегаем промежуточных коллекций: используем sequence
    users.asSequence()
        .filter { it.email.contains("@") }
        .sortedBy { it.name }
        .forEach { u ->
            sb.append("Filtered: ").append(u.name).append('\n')
        }
    return sb.toString()
}

Что изменилось:

  • Явные вызовы append() вместо интерполяции — снижаем количество скрытых StringBuilder.
  • asSequence() — ленивые вычисления без создания промежуточных списков.

Повторное профилирование показывает снижение времени CPU на 60%.

Шаг 4: Профилирование памяти

Используем Memory View в IntelliJ Profiler или VisualVM. Возьмём дамп кучи (heap dump) до и после выполнения функции. Сравните количество экземпляров String и StringBuilder.

До оптимизации: 150 000 объектов String (из-за интерполяции). После: 50 000. Сборка мусора стала происходить реже — приложение дышит легче.

Профилирование корутин и асинхронного кода

Kotlin-корутины добавляют новый уровень сложности. Стандартный CPU-профилировщик может не показывать время, проведённое в suspend-функциях, ожидающих ввода-вывода. Для этого используйте Coroutines Profiler в IntelliJ IDEA Ultimate (доступен с 2022.3).

Пример проблемного кода:



                
                

Блоги

Рекомендаций по книгам