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 {assertDefined} from 'common/assert_utils'; 18import {TimeUtils} from 'common/time_utils'; 19import {ScreenRecordingUtils} from 'trace/screen_recording_utils'; 20import {Timestamp, TimestampType} from 'trace/timestamp'; 21import {TraceEntry} from 'trace/trace'; 22import {Traces} from 'trace/traces'; 23import {TraceEntryFinder} from 'trace/trace_entry_finder'; 24import {TracePosition} from 'trace/trace_position'; 25import {TraceType} from 'trace/trace_type'; 26 27export interface TimeRange { 28 from: Timestamp; 29 to: Timestamp; 30} 31const INVALID_TIMESTAMP = 0n 32 33export class TimelineData { 34 private traces = new Traces(); 35 private screenRecordingVideo?: Blob; 36 private timestampType?: TimestampType; 37 private firstEntry?: TraceEntry<{}>; 38 private lastEntry?: TraceEntry<{}>; 39 private explicitlySetPosition?: TracePosition; 40 private explicitlySetSelection?: TimeRange; 41 private activeViewTraceTypes: TraceType[] = []; // dependencies of current active view 42 43 initialize(traces: Traces, screenRecordingVideo: Blob | undefined) { 44 this.clear(); 45 46 this.traces = new Traces(); 47 traces.forEachTrace((trace, type) => { 48 if (type === TraceType.WINDOW_MANAGER) { 49 // Filter out WindowManager dumps with no timestamp from timeline 50 if ( 51 trace.lengthEntries === 1 && 52 trace.getEntry(0).getTimestamp().getValueNs() === INVALID_TIMESTAMP 53 ) { 54 return; 55 } 56 } 57 58 this.traces.setTrace(type, trace); 59 }); 60 61 this.screenRecordingVideo = screenRecordingVideo; 62 this.firstEntry = this.findFirstEntry(); 63 this.lastEntry = this.findLastEntry(); 64 this.timestampType = this.firstEntry?.getTimestamp().getType(); 65 } 66 67 getCurrentPosition(): TracePosition | undefined { 68 if (this.explicitlySetPosition) { 69 return this.explicitlySetPosition; 70 } 71 const firstActiveEntry = this.getFirstEntryOfActiveViewTraces(); 72 if (firstActiveEntry) { 73 return TracePosition.fromTraceEntry(firstActiveEntry); 74 } 75 if (this.firstEntry) { 76 return TracePosition.fromTraceEntry(this.firstEntry); 77 } 78 return undefined; 79 } 80 81 setPosition(position: TracePosition | undefined) { 82 if (!this.hasTimestamps()) { 83 console.warn('Attempted to set position on traces with no timestamps/entries...'); 84 return; 85 } 86 87 if (position) { 88 if (this.timestampType === undefined) { 89 throw Error('Attempted to set explicit position but no timestamp type is available'); 90 } 91 if (position.timestamp.getType() !== this.timestampType) { 92 throw Error('Attempted to set explicit position with incompatible timestamp type'); 93 } 94 } 95 96 this.explicitlySetPosition = position; 97 } 98 99 setActiveViewTraceTypes(types: TraceType[]) { 100 this.activeViewTraceTypes = types; 101 } 102 103 getTimestampType(): TimestampType | undefined { 104 return this.timestampType; 105 } 106 107 getFullTimeRange(): TimeRange { 108 if (!this.firstEntry || !this.lastEntry) { 109 throw Error('Trying to get full time range when there are no timestamps'); 110 } 111 return { 112 from: this.firstEntry.getTimestamp(), 113 to: this.lastEntry.getTimestamp(), 114 }; 115 } 116 117 getSelectionTimeRange(): TimeRange { 118 if (this.explicitlySetSelection === undefined) { 119 return this.getFullTimeRange(); 120 } else { 121 return this.explicitlySetSelection; 122 } 123 } 124 125 setSelectionTimeRange(selection: TimeRange) { 126 this.explicitlySetSelection = selection; 127 } 128 129 getTraces(): Traces { 130 return this.traces; 131 } 132 133 getScreenRecordingVideo(): Blob | undefined { 134 return this.screenRecordingVideo; 135 } 136 137 searchCorrespondingScreenRecordingTimeSeconds(position: TracePosition): number | undefined { 138 const trace = this.traces.getTrace(TraceType.SCREEN_RECORDING); 139 if (!trace || trace.lengthEntries === 0) { 140 return undefined; 141 } 142 143 const firstTimestamp = trace.getEntry(0).getTimestamp(); 144 const entry = TraceEntryFinder.findCorrespondingEntry(trace, position); 145 if (!entry) { 146 return undefined; 147 } 148 149 return ScreenRecordingUtils.timestampToVideoTimeSeconds(firstTimestamp, entry.getTimestamp()); 150 } 151 152 hasTimestamps(): boolean { 153 return this.firstEntry !== undefined; 154 } 155 156 hasMoreThanOneDistinctTimestamp(): boolean { 157 return ( 158 this.hasTimestamps() && 159 this.firstEntry?.getTimestamp().getValueNs() !== this.lastEntry?.getTimestamp().getValueNs() 160 ); 161 } 162 163 getPreviousEntryFor(type: TraceType): TraceEntry<{}> | undefined { 164 const trace = assertDefined(this.traces.getTrace(type)); 165 if (trace.lengthEntries === 0) { 166 return undefined; 167 } 168 169 const currentIndex = this.findCurrentEntryFor(type)?.getIndex(); 170 if (currentIndex === undefined || currentIndex === 0) { 171 return undefined; 172 } 173 174 return trace.getEntry(currentIndex - 1); 175 } 176 177 getNextEntryFor(type: TraceType): TraceEntry<{}> | undefined { 178 const trace = assertDefined(this.traces.getTrace(type)); 179 if (trace.lengthEntries === 0) { 180 return undefined; 181 } 182 183 const currentIndex = this.findCurrentEntryFor(type)?.getIndex(); 184 if (currentIndex === undefined) { 185 return trace.getEntry(0); 186 } 187 188 if (currentIndex + 1 >= trace.lengthEntries) { 189 return undefined; 190 } 191 192 return trace.getEntry(currentIndex + 1); 193 } 194 195 findCurrentEntryFor(type: TraceType): TraceEntry<{}> | undefined { 196 const position = this.getCurrentPosition(); 197 if (!position) { 198 return undefined; 199 } 200 return TraceEntryFinder.findCorrespondingEntry( 201 assertDefined(this.traces.getTrace(type)), 202 position 203 ); 204 } 205 206 moveToPreviousEntryFor(type: TraceType) { 207 const prevEntry = this.getPreviousEntryFor(type); 208 if (prevEntry !== undefined) { 209 this.setPosition(TracePosition.fromTraceEntry(prevEntry)); 210 } 211 } 212 213 moveToNextEntryFor(type: TraceType) { 214 const nextEntry = this.getNextEntryFor(type); 215 if (nextEntry !== undefined) { 216 this.setPosition(TracePosition.fromTraceEntry(nextEntry)); 217 } 218 } 219 220 clear() { 221 this.traces = new Traces(); 222 this.firstEntry = undefined; 223 this.lastEntry = undefined; 224 this.explicitlySetPosition = undefined; 225 this.timestampType = undefined; 226 this.explicitlySetSelection = undefined; 227 this.screenRecordingVideo = undefined; 228 this.activeViewTraceTypes = []; 229 } 230 231 private findFirstEntry(): TraceEntry<{}> | undefined { 232 let first: TraceEntry<{}> | undefined = undefined; 233 234 this.traces.forEachTrace((trace) => { 235 if (trace.lengthEntries === 0) { 236 return; 237 } 238 const candidate = trace.getEntry(0); 239 if (!first || candidate.getTimestamp() < first.getTimestamp()) { 240 first = candidate; 241 } 242 }); 243 244 return first; 245 } 246 247 private findLastEntry(): TraceEntry<{}> | undefined { 248 let last: TraceEntry<{}> | undefined = undefined; 249 250 this.traces.forEachTrace((trace) => { 251 if (trace.lengthEntries === 0) { 252 return; 253 } 254 const candidate = trace.getEntry(trace.lengthEntries - 1); 255 if (!last || candidate.getTimestamp() > last.getTimestamp()) { 256 last = candidate; 257 } 258 }); 259 260 return last; 261 } 262 263 private getFirstEntryOfActiveViewTraces(): TraceEntry<{}> | undefined { 264 const activeEntries = this.activeViewTraceTypes 265 .filter((it) => this.traces.getTrace(it) !== undefined) 266 .map((traceType) => assertDefined(this.traces.getTrace(traceType))) 267 .filter((trace) => trace.lengthEntries > 0) 268 .map((trace) => trace.getEntry(0)) 269 .sort((a, b) => { 270 return TimeUtils.compareFn(a.getTimestamp(), b.getTimestamp()); 271 }); 272 if (activeEntries.length === 0) { 273 return undefined; 274 } 275 return activeEntries[0]; 276 } 277} 278