1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.tools
18 
19 import kotlin.math.max
20 
21 /**
22  * Time interface with all available timestamp types
23  *
24  * @param elapsedNanos Nanoseconds since boot, including time spent in sleep.
25  * @param systemUptimeNanos Nanoseconds since boot, not counting time spent in deep sleep
26  * @param unixNanos Nanoseconds since Unix epoch
27  */
28 data class Timestamp
29 internal constructor(
30     val elapsedNanos: Long = 0L,
31     val systemUptimeNanos: Long = 0L,
32     val unixNanos: Long = 0L,
33     private val realTimestampFormatter: (Long) -> String
34 ) : Comparable<Timestamp> {
35     val hasElapsedTimestamp = elapsedNanos != 0L
36     val hasSystemUptimeTimestamp = systemUptimeNanos != 0L
37     val hasUnixTimestamp = unixNanos != 0L
38     val isEmpty = !hasElapsedTimestamp && !hasSystemUptimeTimestamp && !hasUnixTimestamp
39     val hasAllTimestamps = hasUnixTimestamp && hasSystemUptimeTimestamp && hasElapsedTimestamp
40     val isMin = elapsedNanos == 1L && systemUptimeNanos == 1L && unixNanos == 1L
41     val isMax =
42         elapsedNanos == Long.MAX_VALUE &&
43             systemUptimeNanos == Long.MAX_VALUE &&
44             unixNanos == Long.MAX_VALUE
45 
unixNanosToLogFormatnull46     fun unixNanosToLogFormat(): String {
47         val seconds = unixNanos / SECOND_AS_NANOSECONDS
48         val nanos = unixNanos % SECOND_AS_NANOSECONDS
49         return "$seconds.${nanos.toString().padStart(9, '0')}"
50     }
51 
toStringnull52     override fun toString(): String {
53         if (isEmpty) {
54             return "<NO TIMESTAMP>"
55         }
56 
57         if (isMin) {
58             return "TIMESTAMP.MIN"
59         }
60 
61         if (isMax) {
62             return "TIMESTAMP.MAX"
63         }
64 
65         return buildString {
66             append("Timestamp(")
67             append(
68                 mutableListOf<String>()
69                     .apply {
70                         if (hasUnixTimestamp) {
71                             add("UNIX=${realTimestampFormatter(unixNanos)}(${unixNanos}ns)")
72                         } else {
73                             add("UNIX=${unixNanos}ns")
74                         }
75                         if (hasSystemUptimeTimestamp) {
76                             add(
77                                 "UPTIME=${formatElapsedTimestamp(systemUptimeNanos)}" +
78                                     "(${systemUptimeNanos}ns)"
79                             )
80                         } else {
81                             add("UPTIME=${systemUptimeNanos}ns")
82                         }
83                         if (hasElapsedTimestamp) {
84                             add(
85                                 "ELAPSED=${formatElapsedTimestamp(elapsedNanos)}(${elapsedNanos}ns)"
86                             )
87                         } else {
88                             add("ELAPSED=${elapsedNanos}ns")
89                         }
90                     }
91                     .joinToString()
92             )
93             append(")")
94         }
95     }
96 
minusnull97     operator fun minus(nanos: Long): Timestamp {
98         val elapsedNanos = max(this.elapsedNanos - nanos, 0L)
99         val systemUptimeNanos = max(this.systemUptimeNanos - nanos, 0L)
100         val unixNanos = max(this.unixNanos - nanos, 0L)
101         return Timestamp(elapsedNanos, systemUptimeNanos, unixNanos, realTimestampFormatter)
102     }
103 
minusnull104     operator fun minus(timestamp: Timestamp): Timestamp {
105         val elapsedNanos =
106             if (this.hasElapsedTimestamp && timestamp.hasElapsedTimestamp) {
107                 this.elapsedNanos - timestamp.elapsedNanos
108             } else {
109                 0L
110             }
111         val systemUptimeNanos =
112             if (this.hasSystemUptimeTimestamp && timestamp.hasSystemUptimeTimestamp) {
113                 this.systemUptimeNanos - timestamp.systemUptimeNanos
114             } else {
115                 0L
116             }
117         val unixNanos =
118             if (this.hasUnixTimestamp && timestamp.hasUnixTimestamp) {
119                 this.unixNanos - timestamp.unixNanos
120             } else {
121                 0L
122             }
123         return Timestamp(elapsedNanos, systemUptimeNanos, unixNanos, realTimestampFormatter)
124     }
125 
126     enum class PreferredType {
127         ELAPSED,
128         SYSTEM_UPTIME,
129         UNIX,
130         ANY
131     }
132 
133     // The preferred and most accurate time type to use when running Timestamp operations or
134     // comparisons
135     private val preferredType: PreferredType
136         get() =
137             when {
138                 hasElapsedTimestamp && hasSystemUptimeTimestamp -> PreferredType.ANY
139                 hasElapsedTimestamp -> PreferredType.ELAPSED
140                 hasSystemUptimeTimestamp -> PreferredType.SYSTEM_UPTIME
141                 hasUnixTimestamp -> PreferredType.UNIX
142                 else -> error("No valid timestamp available")
143             }
144 
equalsnull145     override fun equals(other: Any?): Boolean {
146         if (other !is Timestamp) {
147             return false
148         }
149         return compareTo(other) == 0
150     }
151 
compareTonull152     override fun compareTo(other: Timestamp): Int {
153         var useType = PreferredType.ANY
154         if (other.preferredType == this.preferredType) {
155             useType = this.preferredType
156         } else if (this.preferredType == PreferredType.ANY) {
157             useType = other.preferredType
158         } else if (other.preferredType == PreferredType.ANY) {
159             useType = this.preferredType
160         }
161 
162         return when (useType) {
163             PreferredType.ELAPSED -> this.elapsedNanos.compareTo(other.elapsedNanos)
164             PreferredType.SYSTEM_UPTIME -> this.systemUptimeNanos.compareTo(other.systemUptimeNanos)
165             PreferredType.UNIX,
166             PreferredType.ANY -> {
167                 when {
168                     // If preferred timestamps don't match then comparing UNIX timestamps is
169                     // probably most accurate
170                     this.hasUnixTimestamp && other.hasUnixTimestamp ->
171                         this.unixNanos.compareTo(other.unixNanos)
172                     // Assumes timestamps are collected from the same device
173                     this.hasElapsedTimestamp && other.hasElapsedTimestamp ->
174                         this.elapsedNanos.compareTo(other.elapsedNanos)
175                     this.hasSystemUptimeTimestamp && other.hasSystemUptimeTimestamp ->
176                         this.systemUptimeNanos.compareTo(other.systemUptimeNanos)
177                     else -> error("Timestamps $this and $other are not comparable")
178                 }
179             }
180         }
181     }
182 
hashCodenull183     override fun hashCode(): Int {
184         var result = elapsedNanos.hashCode()
185         result = 31 * result + systemUptimeNanos.hashCode()
186         result = 31 * result + unixNanos.hashCode()
187         return result
188     }
189 
190     companion object {
formatElapsedTimestampnull191         fun formatElapsedTimestamp(timestampNs: Long): String {
192             var remainingNs = timestampNs
193             val prettyTimestamp = StringBuilder()
194 
195             val timeUnitToNanoSeconds =
196                 mapOf(
197                     "d" to DAY_AS_NANOSECONDS,
198                     "h" to HOUR_AS_NANOSECONDS,
199                     "m" to MINUTE_AS_NANOSECONDS,
200                     "s" to SECOND_AS_NANOSECONDS,
201                     "ms" to MILLISECOND_AS_NANOSECONDS,
202                     "ns" to 1,
203                 )
204 
205             for ((timeUnit, ns) in timeUnitToNanoSeconds) {
206                 val convertedTime = remainingNs / ns
207                 remainingNs %= ns
208                 if (prettyTimestamp.isEmpty() && convertedTime == 0L) {
209                     // Trailing 0 unit
210                     continue
211                 }
212                 prettyTimestamp.append("$convertedTime$timeUnit")
213             }
214 
215             if (prettyTimestamp.isEmpty()) {
216                 return "0ns"
217             }
218 
219             return prettyTimestamp.toString()
220         }
221     }
222 }
223