blob: 80949d4957a6a4fffc6212269a7d2447a95f1c23 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.tools.common
import kotlin.js.JsExport
import kotlin.js.JsName
import kotlin.math.max
/**
* Time interface with all available timestamp types
*
* @param elapsedNanos Nanoseconds since boot, including time spent in sleep.
* @param systemUptimeNanos Nanoseconds since boot, not counting time spent in deep sleep
* @param unixNanos Nanoseconds since Unix epoch
*/
@JsExport
data class Timestamp
internal constructor(
val elapsedNanos: Long = 0L,
val systemUptimeNanos: Long = 0L,
val unixNanos: Long = 0L,
private val realTimestampFormatter: (Long) -> String
) : Comparable<Timestamp> {
val hasElapsedTimestamp = elapsedNanos != 0L
val hasSystemUptimeTimestamp = systemUptimeNanos != 0L
val hasUnixTimestamp = unixNanos != 0L
val isEmpty = !hasElapsedTimestamp && !hasSystemUptimeTimestamp && !hasUnixTimestamp
val hasAllTimestamps = hasUnixTimestamp && hasSystemUptimeTimestamp && hasElapsedTimestamp
fun unixNanosToLogFormat(): String {
val seconds = unixNanos / SECOND_AS_NANOSECONDS
val nanos = unixNanos % SECOND_AS_NANOSECONDS
return "$seconds.${nanos.toString().padStart(9, '0')}"
}
override fun toString(): String {
return when {
isEmpty -> "<NO TIMESTAMP>"
hasUnixTimestamp -> "${realTimestampFormatter(unixNanos)} (${unixNanos}ns)"
hasSystemUptimeTimestamp ->
"${formatElapsedTimestamp(systemUptimeNanos)} (${systemUptimeNanos}ns)"
hasElapsedTimestamp -> "${formatElapsedTimestamp(elapsedNanos)} (${elapsedNanos}ns)"
else -> error("Timestamp had no valid timestamps sets")
}
}
@JsName("minusLong")
operator fun minus(nanos: Long): Timestamp {
val elapsedNanos = max(this.elapsedNanos - nanos, 0L)
val systemUptimeNanos = max(this.systemUptimeNanos - nanos, 0L)
val unixNanos = max(this.unixNanos - nanos, 0L)
return Timestamp(elapsedNanos, systemUptimeNanos, unixNanos, realTimestampFormatter)
}
@JsName("minusTimestamp")
operator fun minus(timestamp: Timestamp): Timestamp {
val elapsedNanos =
if (this.hasElapsedTimestamp && timestamp.hasElapsedTimestamp) {
this.elapsedNanos - timestamp.elapsedNanos
} else {
0L
}
val systemUptimeNanos =
if (this.hasSystemUptimeTimestamp && timestamp.hasSystemUptimeTimestamp) {
this.systemUptimeNanos - timestamp.systemUptimeNanos
} else {
0L
}
val unixNanos =
if (this.hasUnixTimestamp && timestamp.hasUnixTimestamp) {
this.unixNanos - timestamp.unixNanos
} else {
0L
}
return Timestamp(elapsedNanos, systemUptimeNanos, unixNanos, realTimestampFormatter)
}
enum class PreferredType {
ELAPSED,
SYSTEM_UPTIME,
UNIX,
ANY
}
// The preferred and most accurate time type to use when running Timestamp operations or
// comparisons
private val preferredType: PreferredType
get() =
when {
hasElapsedTimestamp && hasSystemUptimeTimestamp -> PreferredType.ANY
hasElapsedTimestamp -> PreferredType.ELAPSED
hasSystemUptimeTimestamp -> PreferredType.SYSTEM_UPTIME
hasUnixTimestamp -> PreferredType.UNIX
else -> error("No valid timestamp available")
}
override fun compareTo(other: Timestamp): Int {
var useType = PreferredType.ANY
if (other.preferredType == this.preferredType) {
useType = this.preferredType
} else if (this.preferredType == PreferredType.ANY) {
useType = other.preferredType
} else if (other.preferredType == PreferredType.ANY) {
useType = this.preferredType
}
return when (useType) {
PreferredType.ELAPSED -> this.elapsedNanos.compareTo(other.elapsedNanos)
PreferredType.SYSTEM_UPTIME -> this.systemUptimeNanos.compareTo(other.systemUptimeNanos)
PreferredType.UNIX,
PreferredType.ANY -> {
when {
// If preferred timestamps don't match then comparing UNIX timestamps is
// probably most accurate
this.hasUnixTimestamp && other.hasUnixTimestamp ->
this.unixNanos.compareTo(other.unixNanos)
// Assumes timestamps are collected from the same device
this.hasElapsedTimestamp && other.hasElapsedTimestamp ->
this.elapsedNanos.compareTo(other.elapsedNanos)
this.hasSystemUptimeTimestamp && other.hasSystemUptimeTimestamp ->
this.systemUptimeNanos.compareTo(other.systemUptimeNanos)
else -> error("Timestamps $this and $other are not comparable")
}
}
}
}
companion object {
fun formatElapsedTimestamp(timestampNs: Long): String {
var remainingNs = timestampNs
val prettyTimestamp = StringBuilder()
val timeUnitToNanoSeconds =
mapOf(
"d" to DAY_AS_NANOSECONDS,
"h" to HOUR_AS_NANOSECONDS,
"m" to MINUTE_AS_NANOSECONDS,
"s" to SECOND_AS_NANOSECONDS,
"ms" to MILLISECOND_AS_NANOSECONDS,
"ns" to 1,
)
for ((timeUnit, ns) in timeUnitToNanoSeconds) {
val convertedTime = remainingNs / ns
remainingNs %= ns
if (prettyTimestamp.isEmpty() && convertedTime == 0L) {
// Trailing 0 unit
continue
}
prettyTimestamp.append("$convertedTime$timeUnit")
}
if (prettyTimestamp.isEmpty()) {
return "0ns"
}
return prettyTimestamp.toString()
}
}
}