Профилирование Kotlin кода: находим узкие места на практике
Когда ваш сервер на Kotlin начинает тормозить, а пользователи жалуются на задержки, первое, что приходит в голову — "наверное, проблема в базе данных". Но часто узким местом оказывается сам код: медленные алгоритмы, бесконечные коллекции или неэффективная работа с памятью. В этой статье мы разберем практические инструменты и техники профилирования Kotlin кода, которые помогут вам быстро находить и устранять узкие места.
Почему профилирование важно для Kotlin разработчика?
Kotlin работает поверх JVM, и хотя он избавляет нас от многих проблем Java (например, от NullPointerException), производительность остается под прицелом. Даже простые конструкции вроде map, filter или forEach могут создавать неожиданные накладные расходы, если их неправильно использовать. Профилирование позволяет заглянуть внутрь приложения и понять, какие функции потребляют больше всего CPU или памяти.
В этой статье мы рассмотрим три основных подхода:
- Встроенный профилировщик IntelliJ IDEA (для локальной разработки)
- Утилиты командной строки
jstackиjmap(для production) - Библиотека Hiccup (для программного профилирования)
1. Встроенный профилировщик IntelliJ IDEA — ваш первый помощник
Если вы используете IntelliJ IDEA Ultimate, у вас уже есть мощный инструмент для профилирования. Он работает прямо в IDE и не требует установки дополнительных библиотек.
Как запустить:
- Запустите ваше приложение в режиме отладки (Debug).
- Перейдите на вкладку Profiler (обычно справа внизу).
- Нажмите CPU Profiling или Memory Profiling.
- Остановите профилирование через несколько секунд и проанализируйте Flame Graph (огненный график).
Пример кода для тестирования:
fun main() {
val numbers = (1..1_000_000).toList()
// Узкое место: многократное создание списков
val result = numbers
.filter { it % 2 == 0 }
.map { it * 2 }
.take(100)
println(result)
}
Запустив профилировщик, вы увидите, что на .filter() и .map() уходит значительная часть времени. Решение — использовать Sequences (ленивые последовательности):
fun main() {
val numbers = (1..1_000_000).toList()
// Оптимизированная версия
val result = numbers.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(100)
.toList()
println(result)
}
Профилировщик покажет, что время выполнения сократилось в 2-3 раза.
2. Утилиты командной строки: jstack и jmap для production
Когда приложение уже работает на сервере, вы не можете просто подключить профилировщик IDE. Здесь на помощь приходят утилиты из JDK.
jstack — анализ потоков (Thread Dump)
Используется, когда приложение "зависает" или потребляет 100% CPU. jstack показывает, какие потоки выполняются в данный момент.
Пример использования:
# Найдите PID вашего процесса Kotlin
ps aux | grep kotlin
# Сделайте снимок потоков
jstack -l 12345 > thread_dump.txt
# Анализируйте: ищите блокировки (Lock) и бесконечные циклы
Что искать в выводе:
- Потоки в состоянии
RUNNABLE— они активно используют CPU. - Потоки в состоянии
BLOCKED— они ждут освобождения ресурса (часто из-за синхронизации). - Повторяющиеся вызовы методов — признак бесконечного цикла.
jmap — анализ памяти (Heap Dump)
Если у вас утечка памяти или OutOfMemoryError, jmap поможет найти объекты, которые не освобождаются.
Пример:
# Создайте дамп кучи
jmap -dump:live,format=b,file=heap_dump.hprof 12345
# Откройте в IntelliJ IDEA: File -> Open -> выберите .hprof
Типичная проблема в Kotlin: сохранение ссылок на Activity или Fragment в корутинах. Например:
class MyActivity : AppCompatActivity() {
private val scope = CoroutineScope(Dispatchers.IO)
fun loadData() {
scope.launch {
// Долгая операция
delay(5000)
// После завершения Activity уже уничтожено, но ссылка осталась
updateUI() // Утечка!
}
}
}
Решение — использовать lifecycleScope или отменять корутины при уничтожении Activity.
3. Библиотека Hiccup — программное профилирование для точных измерений
Иногда нужно измерить время выполнения конкретного участка кода в миллисекундах. Для этого существует отличная библиотека Hiccup от компании JetBrains (авторов Kotlin).
Добавьте зависимость в build.gradle.kts:
implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:0.4.10")
Пример использования:
import kotlinx.benchmark.*
@State(Scope.Benchmark)
class MyBenchmark {
private val numbers = (1..1_000_000).toList()
@Benchmark
fun filterAndMap(): List<Int> {
return numbers
.filter { it % 2 == 0 }
.map { it * 2 }
}
@Benchmark
fun sequenceFilterAndMap(): List<Int> {
return numbers.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.toList()
}
}
Запустите бенчмарк через Gradle:
./gradlew benchmark
Результат покажет