From 7f618a6a7cc2fcd19c7a053340bda6d74c6c055a Mon Sep 17 00:00:00 2001 From: Michael Comella Date: Fri, 3 Apr 2020 16:46:48 -0700 Subject: [PATCH] For #8803: add Stat and test. We need to access the data in stat to get the process start time, so we can calculate the time from process start until application.init for the frameworkStart probe. --- .../main/java/org/mozilla/fenix/perf/Stat.kt | 66 +++++++++++++++++++ .../java/org/mozilla/fenix/perf/StatTest.kt | 45 +++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 app/src/main/java/org/mozilla/fenix/perf/Stat.kt create mode 100644 app/src/test/java/org/mozilla/fenix/perf/StatTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/perf/Stat.kt b/app/src/main/java/org/mozilla/fenix/perf/Stat.kt new file mode 100644 index 0000000000..a2ff22b53b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/Stat.kt @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import android.os.SystemClock +import android.system.Os +import android.system.OsConstants +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.PRIVATE +import java.io.File +import java.util.concurrent.TimeUnit + +private const val FIELD_POS_STARTTIME = 21 // starttime naming matches field in man page. + +/** + * Functionality from stat on the proc pseudo-filesystem common to unix systems. /proc contains + * information related to active processes. /proc/$pid/stat contains information about the status of + * the process by the given process id (pid). + * + * See the man page - `man 5 proc` - on linux for more information: + * http://man7.org/linux/man-pages/man5/proc.5.html + */ +open class Stat { + + @VisibleForTesting(otherwise = PRIVATE) + open fun getStatText(pid: Int): String = File("/proc/$pid/stat").readText() + + // See `man 3 sysconf` for details on Os.sysconf and OsConstants: + // http://man7.org/linux/man-pages/man3/sysconf.3.html + open val clockTicksPerSecond: Long get() = Os.sysconf(OsConstants._SC_CLK_TCK) + private val nanosPerClockTick = TimeUnit.SECONDS.toNanos(1).let { nanosPerSecond -> + // We use nanos per clock tick, rather than clock ticks per nanos, to mitigate float/double + // rounding errors: this way we can use integer values and divide the larger value by the smaller one. + nanosPerSecond / clockTicksPerSecond.toDouble() + } + + /** + * Gets the process start time since system boot in ticks, including time spent in suspension/deep sleep. + * This value can be compared against [SystemClock.elapsedRealtimeNanos]: you can convert between + * measurements using [convertTicksToNanos] and [convertNanosToTicks]. + * + * Ticks are "an arbitrary unit for measuring internal system time": https://superuser.com/a/101202 + * They are not aligned with CPU frequency and do not change at runtime but can theoretically + * change between devices. On the Pixel 2, one tick is equivalent to one centisecond. + * + * We confirmed that this measurement and elapsedRealtimeNanos both include suspension time, and + * are thus comparable, by* looking at their source: + * - /proc/pid/stat starttime is set using boottime: + * https://github.com/torvalds/linux/blob/79e178a57dae819ae724065b47c25720494cc9f2/fs/proc/array.c#L536 + * - elapsedRealtimeNanos is set using boottime: + * https://cs.android.com/android/platform/superproject/+/master:system/core/libutils/SystemClock.cpp;l=60-68;drc=bab16584ce0525742b5370682c9132b2002ee110 + * + * Perf note: this call reads from the pseudo-filesystem using the java File APIs, which isn't + * likely to be a very optimized call path. + * + * Implementation inspired by https://stackoverflow.com/a/42195623. + */ + fun getProcessStartTimeTicks(pid: Int): Long { + return getStatText(pid).split(' ')[FIELD_POS_STARTTIME].toLong() + } + + fun convertTicksToNanos(ticks: Long): Double = ticks * nanosPerClockTick + fun convertNanosToTicks(nanos: Long): Double = nanos / nanosPerClockTick +} diff --git a/app/src/test/java/org/mozilla/fenix/perf/StatTest.kt b/app/src/test/java/org/mozilla/fenix/perf/StatTest.kt new file mode 100644 index 0000000000..aed5e6c71e --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/perf/StatTest.kt @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +private const val STAT_CONTENTS = "32250 (a.fennec_aurora) S 831 831 0 0 -1 1077952832 670949 0 184936 0 15090 5387 0 0 20 0 119 0 166636813 9734365184 24664 18446744073709551615 1 1 0 0 0 0 4612 4097 1073792254 0 0 0 17 1 0 0 0 0 0 0 0 0 0 0 0 0 0" +private const val CLOCK_TICKS_PER_SECOND = 100L // actual value on the Pixel 2. + +class StatTest { + + private lateinit var stat: StatTestImpl + + @Before + fun setUp() { + stat = StatTestImpl() + } + + @Test + fun `WHEN getting the process start time THEN the correct value is returned`() { + val actual = stat.getProcessStartTimeTicks(pid = -1) // pid behavior is overridden. + assertEquals(166636813, actual) // expected value calculated by hand. + } + + @Test + fun `WHEN converting ticks to nanos THEN the correct value is returned`() { + val actual = stat.convertTicksToNanos(166_636_813) + assertEquals(1_666_368_130_000_000.0, actual, 0.0) // expected value calculated by hand. + } + + @Test + fun `WHEN converting nanos to ticks THEN the correct value is returned`() { + val actual = stat.convertNanosToTicks(1_666_368_135_432_102) + assertEquals(166_636_813.5432102, actual, 0.0) // expected value calculated by hand. + } +} + +class StatTestImpl : Stat() { + override fun getStatText(pid: Int): String = STAT_CONTENTS + override val clockTicksPerSecond: Long get() = CLOCK_TICKS_PER_SECOND +}