1/* 2 * Copyright (C) 2022 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 17import {ElapsedTimestamp, RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp'; 18 19export class TimeUtils { 20 static compareFn(a: Timestamp, b: Timestamp): number { 21 if (a.getType() !== b.getType()) { 22 throw new Error('Attempted to compare two timestamps with different type'); 23 } 24 return Number(a.getValueNs() - b.getValueNs()); 25 } 26 27 static format(timestamp: Timestamp, hideNs = false): string { 28 switch (timestamp.getType()) { 29 case TimestampType.ELAPSED: { 30 return TimeUtils.nanosecondsToHumanElapsed(timestamp.getValueNs(), hideNs); 31 } 32 case TimestampType.REAL: { 33 return TimeUtils.nanosecondsToHumanReal(timestamp.getValueNs(), hideNs); 34 } 35 default: { 36 throw Error('Unhandled timestamp type'); 37 } 38 } 39 } 40 41 private static nanosecondsToHumanElapsed(timestampNanos: number | bigint, hideNs = true): string { 42 timestampNanos = BigInt(timestampNanos); 43 const units = TimeUtils.units; 44 45 let leftNanos = timestampNanos; 46 const parts: Array<{value: bigint; unit: string}> = units 47 .slice() 48 .reverse() 49 .map(({nanosInUnit, unit}) => { 50 let amountOfUnit = BigInt(0); 51 if (leftNanos >= nanosInUnit) { 52 amountOfUnit = leftNanos / BigInt(nanosInUnit); 53 } 54 leftNanos = leftNanos % BigInt(nanosInUnit); 55 return {value: amountOfUnit, unit}; 56 }); 57 58 if (hideNs) { 59 parts.pop(); 60 } 61 62 // Remove all 0ed units at start 63 while (parts.length > 1 && parts[0].value === 0n) { 64 parts.shift(); 65 } 66 67 return parts.map((part) => `${part.value}${part.unit}`).join(''); 68 } 69 70 private static nanosecondsToHumanReal(timestampNanos: number | bigint, hideNs = true): string { 71 timestampNanos = BigInt(timestampNanos); 72 const ms = timestampNanos / 1000000n; 73 const extraNanos = timestampNanos % 1000000n; 74 const formattedTimestamp = new Date(Number(ms)).toISOString().replace('Z', ''); 75 76 if (hideNs) { 77 return formattedTimestamp; 78 } else { 79 return `${formattedTimestamp}${extraNanos.toString().padStart(6, '0')}`; 80 } 81 } 82 83 static parseHumanElapsed(timestampHuman: string): Timestamp { 84 if (!TimeUtils.HUMAN_ELAPSED_TIMESTAMP_REGEX.test(timestampHuman)) { 85 throw Error('Invalid elapsed timestamp format'); 86 } 87 88 const units = TimeUtils.units; 89 90 const usedUnits = timestampHuman.split(/[0-9]+/).filter((it) => it !== ''); 91 const usedValues = timestampHuman 92 .split(/[a-z]+/) 93 .filter((it) => it !== '') 94 .map((it) => Math.floor(Number(it))); 95 96 let ns = BigInt(0); 97 98 for (let i = 0; i < usedUnits.length; i++) { 99 const unit = usedUnits[i]; 100 const value = usedValues[i]; 101 const unitData = units.find((it) => it.unit === unit)!; 102 ns += BigInt(unitData.nanosInUnit) * BigInt(value); 103 } 104 105 return new ElapsedTimestamp(ns); 106 } 107 108 static parseHumanReal(timestampHuman: string): Timestamp { 109 if (!TimeUtils.HUMAN_REAL_TIMESTAMP_REGEX.test(timestampHuman)) { 110 throw Error('Invalid real timestamp format'); 111 } 112 113 // Add trailing Z if it isn't there yet 114 if (timestampHuman[timestampHuman.length - 1] !== 'Z') { 115 timestampHuman += 'Z'; 116 } 117 118 // Date.parse only considers up to millisecond precision 119 let nanoSeconds = 0; 120 if (timestampHuman.includes('.')) { 121 const milliseconds = timestampHuman.split('.')[1].replace('Z', ''); 122 nanoSeconds = Math.floor(Number(milliseconds.padEnd(9, '0').slice(3))); 123 } 124 125 return new RealTimestamp( 126 BigInt(Date.parse(timestampHuman)) * BigInt(TimeUtils.TO_NANO['ms']) + BigInt(nanoSeconds) 127 ); 128 } 129 130 static formattedKotlinTimestamp(time: any, timestampType: TimestampType): string { 131 if (timestampType === TimestampType.ELAPSED) { 132 return TimeUtils.format(new ElapsedTimestamp(BigInt(time.elapsedNanos.toString()))); 133 } 134 135 if (timestampType === TimestampType.REAL) { 136 return TimeUtils.format(new RealTimestamp(BigInt(time.unixNanos.toString()))); 137 } 138 139 throw new Error('Unsupported timestamp'); 140 } 141 142 static TO_NANO = { 143 ns: 1, 144 ms: 1000000, 145 s: 1000000 * 1000, 146 m: 1000000 * 1000 * 60, 147 h: 1000000 * 1000 * 60 * 60, 148 d: 1000000 * 1000 * 60 * 60 * 24, 149 }; 150 151 static units = [ 152 {nanosInUnit: TimeUtils.TO_NANO['ns'], unit: 'ns'}, 153 {nanosInUnit: TimeUtils.TO_NANO['ms'], unit: 'ms'}, 154 {nanosInUnit: TimeUtils.TO_NANO['s'], unit: 's'}, 155 {nanosInUnit: TimeUtils.TO_NANO['m'], unit: 'm'}, 156 {nanosInUnit: TimeUtils.TO_NANO['h'], unit: 'h'}, 157 {nanosInUnit: TimeUtils.TO_NANO['d'], unit: 'd'}, 158 ]; 159 160 // (?=.) checks there is at least one character with a lookahead match 161 static readonly HUMAN_ELAPSED_TIMESTAMP_REGEX = 162 /^(?=.)([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?([0-9]+ms)?([0-9]+ns)?$/; 163 static readonly HUMAN_REAL_TIMESTAMP_REGEX = 164 /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])\.[0-9]{3}([0-9]{6})?Z?$/; 165 static readonly NS_TIMESTAMP_REGEX = /^\s*[0-9]+(\s?ns)?\s*$/; 166} 167