[Разработка мобильных приложений, Разработка под Android, Kotlin] Пишем свой профайлер для анализа производительности приложения на Android
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
По мере развития приложения стоит проводить её аудит для выявления неявных деградаций в производительности. Недавно я проводил аудит раздела комментариев iFunny и написал собственный профайлер. Он не заменит имеющиеся на рынке инструменты Android Profile из Android Studio, Battery Historian и Systrace, но обладает рядом плюсов:
- Негативное влияние профилировщика на производительность приложения сводится к минимуму
- Документация итераций оптимизации работы приложения.
- Гибкость в сборе метрик.
Далее расскажу про существующие инструменты подробнее и перейдём к анализу использования процессорного времени, памяти, использования сети и потребления батареи с помощью кастомного решения.Общеизвестные инструментыИх не так много, основные я уже перечислил, рассмотрим их подробнее.1. Android Profiler. Встроен в Android Studio, он позволяет отслеживать потребление CPU, Memory, Network и Energy во время использования приложения и предоставляет много полезных инструментов. Но у него есть как плюсы, так и минусы.Плюсы:
- Наглядные графики.
- Довольно обширный функционал.
- Централизованность, всё в одном месте.
Минусы:
- В работе приложения появляются тормоза из-за механизмов сбора метрик, это не позволяет контролировать основные параметры потребления ресурсов, приближённые в использовании к реальным условиям.
- Приложение может спонтанно крашиться.
- Отсутствие гибкости в сборке метрик. Например, чтобы не дампить постоянно heap память на критических показателях, хочется, чтобы Android Profiler делал это за тебя.
2. Systrace. Инструмент, позволяющий собирать и инспектировать время работы частей кода по всем процессам на девайсе.3. Battery Historian. Нужен, чтобы получить дополнительную информацию по работе приложения, например, процент потребления батарейки и условия, влияющие на потребление.Изначально основным инструментом для профилирования я выбрал Android Profiler, но его минусы показались критичными. Хочется показать наглядно и количественно с наименьшей погрешностью этапы оптимизации комментариев. Поэтому решил написать свой профилировщик и строить на его основе графики в любом удобном инструменте, например, Excel или Numbers.В качестве движка по выполнению долгих рутинных повторяющихся действий можно использовать фреймворк автотестов Espresso.CPUПолучить процент использования процессорного времени процессом приложения довольно сложная задача, но так как Android разработан на базе операционной системы Linux, то можно выполнить специальную команду, распарсить ответ и зафиксировать полученное значение.Например, можно использовать команду /proc/<PID>/stat , а PID получить из android.os.Process.myPid(). Но в Android O ограничили доступ к /proc, что заставило использовать другую команду top -n 1, где -n - количество итераций обновлений. Ниже представлен код, фиксирующий значение потребления CPU соответствующего процесса.
class CpuUsageExporter(context: Context) : AppMetricExporter {
private companion object {
const val CPU_USAGE_FILENAME = "cpu_usage.txt"
const val PACKAGE_NAME = "com.example.app"
}
private val cpuPw = PrintWriter(FileOutputStream(File(context.filesDir, CPU_USAGE_FILENAME), true), true)
override fun export() {
try {
recordCpu()
} catch (th: Throwable) {
Assert.fail(th)
}
}
override fun close() {
cpuPw.close()
}
private fun recordCpu() {
val processLine = readSystemFile("top", "-n", "1").filter { it.contains(PACKAGE_NAME) }
.flatMap { it.split(" ") }
.map(String::trim)
.filter(String::isNotEmpty)
if (processLine.isNotEmpty()) {
val index = processLine.indexOfFirst { it == "S" || it == "R" || it == "D" }
check(index > -1) {
"Not found process state of $PACKAGE_NAME"
}
cpuPw.println(processLine[index + 1].toFloat().toInt().toString())
}
}
@Throws(java.lang.Exception::class)
private fun readSystemFile(vararg pSystemFile: String): List<String> {
return Runtime.getRuntime()
.exec(pSystemFile).inputStream.bufferedReader()
.useLines {
it.toList()
}
}
}
recordCpu() завернут в try-catch, так как на некоторых девайсах могут возникать ошибки. Все полученные значения записываются в cpu_usage.txt, откуда берутся данные для построения аналитической информации и построения графиков.MemoryС памятью всё довольно просто, есть классы Debug и Runtime, которые предоставляют необходимый функционал. Если heap память заполняется на 90%, то запускается механизм дампинга памяти автоматически. Таким образом, разработчику не нужно следить за ходом выполнения тестов и он может спокойно пойти попить кофе. После выполненных тестов дамп можно открыть с помощью Android Profiler в Android Studio (Profiler → + → Load from file). Ниже представлен код, анализирующий потребление памяти приложением.
class MemoryUsageExporter(context: Context) : AppMetricExporter {
private companion object {
const val MEM_USAGE_FILENAME = "mem_usage.txt"
const val CRITICAL_MEMORY_LOADING = 0.9
}
private val absolutePath = context.filesDir.absolutePath
private val memPw = PrintWriter(FileOutputStream(File(context.filesDir, MEM_USAGE_FILENAME), true), true)
override fun export() {
val runtime = Runtime.getRuntime()
val maxHeapSizeInMB = InformationUnit.BYTE.toMB(runtime.maxMemory())
val availHeapSizeInMB = InformationUnit.BYTE.toMB(runtime.freeMemory())
val usedHeapSizeInMB = InformationUnit.BYTE.toMB((runtime.totalMemory() - runtime.freeMemory()))
val totalNativeMemorySize = InformationUnit.BYTE.toMB(Debug.getNativeHeapSize())
val availNativeMemoryFreeSize = InformationUnit.BYTE.toMB(Debug.getNativeHeapFreeSize())
val usedNativeMemoryInMb = InformationUnit.BYTE.toMB(totalNativeMemorySize - availNativeMemoryFreeSize)
if (usedHeapSizeInMB > maxHeapSizeInMB * CRITICAL_MEMORY_LOADING) {
Debug.dumpHprofData("$absolutePath/dump_heap_memory_${System.currentTimeMillis()}.hprof")
}
val str =
"$usedHeapSizeInMB $availHeapSizeInMB $maxHeapSizeInMB $usedNativeMemoryInMb $availNativeMemoryFreeSize $totalNativeMemorySize"
memPw.println(str)
}
override fun close() {
memPw.close()
}
}
Таким образом, в файле mem_usage.txt образовывается несколько колонок с необходимой информацией, которую можно анализировать по-разному с аналитической точки зрения.NetworkДля метрик использования сети доступен класс android.net.TrafficStats, при помощи которого можно получить принятый и переданный трафик. Ниже представлен код фиксирования трафика данных. Все данные записываются в файл network_usage.txt.
class NetworkUsageExporter(context: Context) : AppMetricExporter {
private companion object {
const val NETWORK_USAGE_FILENAME = "network_usage.txt"
}
private val networkPw = PrintWriter(FileOutputStream(File(context.filesDir, NETWORK_USAGE_FILENAME), true), true)
private var transmittedBytes = 0L
private var receivedBytes = 0L
override fun export() {
val tBytes = TrafficStats.getTotalTxBytes()
val rBytes = TrafficStats.getTotalRxBytes()
if (tBytes.toInt() == TrafficStats.UNSUPPORTED || rBytes.toInt() == TrafficStats.UNSUPPORTED) {
throw RuntimeException("Device not support network monitoring")
} else if (transmittedBytes > 0 && receivedBytes > 0) {
networkPw.println("${tBytes - transmittedBytes} ${rBytes - receivedBytes}")
}
transmittedBytes = tBytes
receivedBytes = rBytes
}
override fun close() {
networkPw.close()
}
}
BatteryПолучение метрик потребления батарейки также довольно простая задача. BatteryManager хранит текущий статус заряда батареи в sticky Intent, который можно получить, если передать в метод registerReceiver IntentFilter c action Intent.ACTION_BATTERY_CHANGED без регистрации ресивера (его, конечно, можно передать, но в данной задачи этого не требуется).Ниже код получения метрик использования батареи.
class BatteryUsageExporter(private val context: Context) : AppMetricExporter {
private companion object {
const val BATTERY_USAGE_FILENAME = "battery_usage.txt"
const val INVALIDATE_VALUE = -1
}
private val batteryPw = PrintWriter(FileOutputStream(File(context.filesDir, BATTERY_USAGE_FILENAME), true), true)
private val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
override fun export() {
val batteryStatus: Intent? = intentFilter.let { ifilter -> context.registerReceiver(null, ifilter) }
val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, INVALIDATE_VALUE) ?: INVALIDATE_VALUE
if (status != INVALIDATE_VALUE) {
batteryPw.println("$status")
}
}
override fun close() {
batteryPw.close()
}
}
Все экспортёры метрик управляются AppMetricUsageManager. Раз в секунду происходит анализ потребления ресурсов и соответствующие значения заносятся по файлами, указанных в экспортёрах. Его код довольно прост:
class AppMetricUsageManager(context: Context) {
private companion object {
const val INTERVAL_TIME_IN_SEC = 1L
const val INITIAL_DELAY = 0L
}
private val exporters = listOf(CpuUsageExporter(context),
MemoryUsageExporter(context),
BatteryUsageExporter(context),
NetworkUsageExporter(context))
private var disposable: Disposable? = null
fun startCollect() {
disposable = Observable.interval(INITIAL_DELAY, INTERVAL_TIME_IN_SEC, TimeUnit.SECONDS)
.subscribe({ exporters.forEach { it.export() } }, { th -> Timber.e(th) })
}
fun stopCollect() {
exporters.forEach { it.close() }
disposable.safeDispose()
disposable = null
}
}
safeDispose() — экстеншен, который выполняет dispose, если Observable не null или не задиспоузен ранее.После проведённых тестов можно получить красивые графики:
Возникает резонный вопрос. А зачем все это, если есть профайлер?Как я и говорил в начале статьи, у кастомного решения есть ряд преимуществ по отношению к Android Profiler — это отсутствие крашей, замедлений приложения, гибкость в сборе метрик и составление документации для анализа хода выполнения оптимизаций. В тоже время данный подход не является заменой Android Profiler, Systrace или Battery Historian, а лишь дополняет их.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Разработка под iOS, Разработка под Android, Big Data] Разработка большого проекта за 6 месяцев: как не облажаться
- [IT-эмиграция, Карьера в IT-индустрии, IT-компании] [Личная история] Из Москвы — в Кремниевую долину. Как пройти в Google, и почему здесь нужно уметь играть в покер
- [IT-инфраструктура, Аналитика мобильных приложений, Инженерные системы] Мобильные терминалы регистрации: обзор приложений
- [Анализ и проектирование систем, Разработка под Android] «Оливье в каждой семьей свой»: или как мы придумали ещё одну многомодульную архитектуру
- [.NET, API, Google API, C#, DIY или Сделай сам] How to be good in hackathons as a developer? Practice creating simple pet projects
- [Информационная безопасность, Обработка изображений, IT-стандарты, Математика] Формат JPEG XL будет полным по Тьюрингу без ограничения 1024*1024 пикселей
- [IT-стандарты, Карьера в IT-индустрии, IT-компании] Google закрыла программу поддержки молодых инженеров из-за жалоб на неравенство в жалованье
- [IT-стандарты, Карьера в IT-индустрии, Здоровье, IT-компании] Google завела внутренний инструмент для вычисления зарплаты переехавших удалёнщиков
- [Информационная безопасность, Администрирование доменных имен, Законодательство в IT] РКН потребовал от Google прекратить поддержку сайта «Умное голосование»
- Доступна сборка Android-x86 8.1-r6
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_razrabotka_mobilnyh_prilozhenij (разработка мобильных приложений), #_android, #_kotlin, #_android_profiler, #_profajler (профайлер), #_android (андроид), #_mobilnye_prilozhenija (мобильные приложения), #_google, #_blog_kompanii_funcorp (
Блог компании FunCorp
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:29
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
По мере развития приложения стоит проводить её аудит для выявления неявных деградаций в производительности. Недавно я проводил аудит раздела комментариев iFunny и написал собственный профайлер. Он не заменит имеющиеся на рынке инструменты Android Profile из Android Studio, Battery Historian и Systrace, но обладает рядом плюсов:
class CpuUsageExporter(context: Context) : AppMetricExporter {
private companion object { const val CPU_USAGE_FILENAME = "cpu_usage.txt" const val PACKAGE_NAME = "com.example.app" } private val cpuPw = PrintWriter(FileOutputStream(File(context.filesDir, CPU_USAGE_FILENAME), true), true) override fun export() { try { recordCpu() } catch (th: Throwable) { Assert.fail(th) } } override fun close() { cpuPw.close() } private fun recordCpu() { val processLine = readSystemFile("top", "-n", "1").filter { it.contains(PACKAGE_NAME) } .flatMap { it.split(" ") } .map(String::trim) .filter(String::isNotEmpty) if (processLine.isNotEmpty()) { val index = processLine.indexOfFirst { it == "S" || it == "R" || it == "D" } check(index > -1) { "Not found process state of $PACKAGE_NAME" } cpuPw.println(processLine[index + 1].toFloat().toInt().toString()) } } @Throws(java.lang.Exception::class) private fun readSystemFile(vararg pSystemFile: String): List<String> { return Runtime.getRuntime() .exec(pSystemFile).inputStream.bufferedReader() .useLines { it.toList() } } } class MemoryUsageExporter(context: Context) : AppMetricExporter {
private companion object { const val MEM_USAGE_FILENAME = "mem_usage.txt" const val CRITICAL_MEMORY_LOADING = 0.9 } private val absolutePath = context.filesDir.absolutePath private val memPw = PrintWriter(FileOutputStream(File(context.filesDir, MEM_USAGE_FILENAME), true), true) override fun export() { val runtime = Runtime.getRuntime() val maxHeapSizeInMB = InformationUnit.BYTE.toMB(runtime.maxMemory()) val availHeapSizeInMB = InformationUnit.BYTE.toMB(runtime.freeMemory()) val usedHeapSizeInMB = InformationUnit.BYTE.toMB((runtime.totalMemory() - runtime.freeMemory())) val totalNativeMemorySize = InformationUnit.BYTE.toMB(Debug.getNativeHeapSize()) val availNativeMemoryFreeSize = InformationUnit.BYTE.toMB(Debug.getNativeHeapFreeSize()) val usedNativeMemoryInMb = InformationUnit.BYTE.toMB(totalNativeMemorySize - availNativeMemoryFreeSize) if (usedHeapSizeInMB > maxHeapSizeInMB * CRITICAL_MEMORY_LOADING) { Debug.dumpHprofData("$absolutePath/dump_heap_memory_${System.currentTimeMillis()}.hprof") } val str = "$usedHeapSizeInMB $availHeapSizeInMB $maxHeapSizeInMB $usedNativeMemoryInMb $availNativeMemoryFreeSize $totalNativeMemorySize" memPw.println(str) } override fun close() { memPw.close() } } class NetworkUsageExporter(context: Context) : AppMetricExporter {
private companion object { const val NETWORK_USAGE_FILENAME = "network_usage.txt" } private val networkPw = PrintWriter(FileOutputStream(File(context.filesDir, NETWORK_USAGE_FILENAME), true), true) private var transmittedBytes = 0L private var receivedBytes = 0L override fun export() { val tBytes = TrafficStats.getTotalTxBytes() val rBytes = TrafficStats.getTotalRxBytes() if (tBytes.toInt() == TrafficStats.UNSUPPORTED || rBytes.toInt() == TrafficStats.UNSUPPORTED) { throw RuntimeException("Device not support network monitoring") } else if (transmittedBytes > 0 && receivedBytes > 0) { networkPw.println("${tBytes - transmittedBytes} ${rBytes - receivedBytes}") } transmittedBytes = tBytes receivedBytes = rBytes } override fun close() { networkPw.close() } } class BatteryUsageExporter(private val context: Context) : AppMetricExporter {
private companion object { const val BATTERY_USAGE_FILENAME = "battery_usage.txt" const val INVALIDATE_VALUE = -1 } private val batteryPw = PrintWriter(FileOutputStream(File(context.filesDir, BATTERY_USAGE_FILENAME), true), true) private val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) override fun export() { val batteryStatus: Intent? = intentFilter.let { ifilter -> context.registerReceiver(null, ifilter) } val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, INVALIDATE_VALUE) ?: INVALIDATE_VALUE if (status != INVALIDATE_VALUE) { batteryPw.println("$status") } } override fun close() { batteryPw.close() } } class AppMetricUsageManager(context: Context) {
private companion object { const val INTERVAL_TIME_IN_SEC = 1L const val INITIAL_DELAY = 0L } private val exporters = listOf(CpuUsageExporter(context), MemoryUsageExporter(context), BatteryUsageExporter(context), NetworkUsageExporter(context)) private var disposable: Disposable? = null fun startCollect() { disposable = Observable.interval(INITIAL_DELAY, INTERVAL_TIME_IN_SEC, TimeUnit.SECONDS) .subscribe({ exporters.forEach { it.export() } }, { th -> Timber.e(th) }) } fun stopCollect() { exporters.forEach { it.close() } disposable.safeDispose() disposable = null } } Возникает резонный вопрос. А зачем все это, если есть профайлер?Как я и говорил в начале статьи, у кастомного решения есть ряд преимуществ по отношению к Android Profiler — это отсутствие крашей, замедлений приложения, гибкость в сборе метрик и составление документации для анализа хода выполнения оптимизаций. В тоже время данный подход не является заменой Android Profiler, Systrace или Battery Historian, а лишь дополняет их. =========== Источник: habr.com =========== Похожие новости:
Блог компании FunCorp ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 21:29
Часовой пояс: UTC + 5