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 {TimeRange, Timestamp} from 'common/time'; 18import {ComponentTimestampConverter} from 'common/timestamp_converter'; 19import {ScreenRecordingUtils} from 'trace/screen_recording_utils'; 20import {Trace, TraceEntry} from 'trace/trace'; 21import {Traces} from 'trace/traces'; 22import {TraceEntryFinder} from 'trace/trace_entry_finder'; 23import {TracePosition} from 'trace/trace_position'; 24import {TraceType, TraceTypeUtils} from 'trace/trace_type'; 25import {PropertyTreeNode} from 'trace/tree_node/property_tree_node'; 26 27export class TimelineData { 28 private traces = new Traces(); 29 private screenRecordingVideo?: Blob; 30 private firstEntry?: TraceEntry<object>; 31 private lastEntry?: TraceEntry<object>; 32 private explicitlySetPosition?: TracePosition; 33 private explicitlySetSelection?: TimeRange; 34 private explicitlySetZoomRange?: TimeRange; 35 private lastReturnedCurrentPosition?: TracePosition; 36 private lastReturnedFullTimeRange?: TimeRange; 37 private lastReturnedCurrentEntries = new Map< 38 Trace<object>, 39 TraceEntry<any> | undefined 40 >(); 41 private activeTrace: Trace<object> | undefined; 42 private transitions: PropertyTreeNode[] = []; // cached trace entries to avoid TP and object creation latencies each time transition timeline is redrawn 43 private timestampConverter: ComponentTimestampConverter | undefined; 44 45 async initialize( 46 traces: Traces, 47 screenRecordingVideo: Blob | undefined, 48 timestampConverter: ComponentTimestampConverter, 49 ) { 50 this.clear(); 51 52 this.timestampConverter = timestampConverter; 53 54 this.traces = new Traces(); 55 traces.forEachTrace((trace, type) => { 56 // Filter out dumps with invalid timestamp (would mess up the timeline) 57 if (trace.isDumpWithoutTimestamp()) { 58 return; 59 } 60 61 this.traces.addTrace(trace); 62 }); 63 64 const transitionTrace = this.traces.getTrace(TraceType.TRANSITION); 65 if (transitionTrace) { 66 this.transitions = await Promise.all( 67 transitionTrace.mapEntry(async (entry) => await entry.getValue()), 68 ); 69 } 70 71 this.screenRecordingVideo = screenRecordingVideo; 72 this.firstEntry = this.findFirstEntry(); 73 this.lastEntry = this.findLastEntry(); 74 75 const tracesSortedByDisplayOrder = traces 76 .mapTrace((trace) => trace) 77 .filter( 78 (trace) => 79 TraceTypeUtils.isTraceTypeWithViewer(trace.type) && 80 trace.type !== TraceType.SCREEN_RECORDING, 81 ) 82 .sort((a, b) => { 83 return TraceTypeUtils.compareByDisplayOrder(a.type, b.type); 84 }); 85 if (tracesSortedByDisplayOrder.length > 0) { 86 this.trySetActiveTrace(tracesSortedByDisplayOrder[0]); 87 } 88 } 89 90 getTransitions(): PropertyTreeNode[] { 91 return this.transitions; 92 } 93 94 getTimestampConverter(): ComponentTimestampConverter | undefined { 95 return this.timestampConverter; 96 } 97 98 getCurrentPosition(): TracePosition | undefined { 99 if (this.explicitlySetPosition) { 100 return this.explicitlySetPosition; 101 } 102 103 let currentPosition: TracePosition | undefined = undefined; 104 if (this.firstEntry) { 105 currentPosition = TracePosition.fromTraceEntry(this.firstEntry); 106 } 107 108 const firstActiveEntry = this.getFirstEntryOfActiveViewTrace(); 109 if (firstActiveEntry) { 110 currentPosition = TracePosition.fromTraceEntry(firstActiveEntry); 111 } 112 113 if ( 114 this.lastReturnedCurrentPosition === undefined || 115 currentPosition === undefined || 116 !this.lastReturnedCurrentPosition.isEqual(currentPosition) 117 ) { 118 this.lastReturnedCurrentPosition = currentPosition; 119 } 120 121 return this.lastReturnedCurrentPosition; 122 } 123 124 setPosition(position: TracePosition | undefined) { 125 if (!this.hasTimestamps()) { 126 console.warn( 127 'Attempted to set position on traces with no timestamps/entries...', 128 ); 129 return; 130 } 131 132 this.explicitlySetPosition = position; 133 } 134 135 makePositionFromActiveTrace(timestamp: Timestamp): TracePosition { 136 if (!this.activeTrace) { 137 return TracePosition.fromTimestamp(timestamp); 138 } 139 140 const entry = this.activeTrace.findClosestEntry(timestamp); 141 if (!entry) { 142 return TracePosition.fromTimestamp(timestamp); 143 } 144 145 return TracePosition.fromTraceEntry(entry, timestamp); 146 } 147 148 trySetActiveTrace(trace: Trace<object>): boolean { 149 const isTraceWithValidTimestamps = this.traces.hasTrace(trace); 150 if (this.activeTrace !== trace && isTraceWithValidTimestamps) { 151 this.activeTrace = trace; 152 return true; 153 } 154 return false; 155 } 156 157 getActiveTrace() { 158 return this.activeTrace; 159 } 160 161 getFullTimeRange(): TimeRange { 162 if (!this.firstEntry || !this.lastEntry) { 163 throw Error('Trying to get full time range when there are no timestamps'); 164 } 165 166 const fullTimeRange = new TimeRange( 167 this.firstEntry.getTimestamp(), 168 this.lastEntry.getTimestamp(), 169 ); 170 171 if ( 172 this.lastReturnedFullTimeRange === undefined || 173 this.lastReturnedFullTimeRange.from.getValueNs() !== 174 fullTimeRange.from.getValueNs() || 175 this.lastReturnedFullTimeRange.to.getValueNs() !== 176 fullTimeRange.to.getValueNs() 177 ) { 178 this.lastReturnedFullTimeRange = fullTimeRange; 179 } 180 181 return this.lastReturnedFullTimeRange; 182 } 183 184 getSelectionTimeRange(): TimeRange { 185 if (this.explicitlySetSelection === undefined) { 186 return this.getFullTimeRange(); 187 } else { 188 return this.explicitlySetSelection; 189 } 190 } 191 192 setSelectionTimeRange(selection: TimeRange) { 193 this.explicitlySetSelection = selection; 194 } 195 196 getZoomRange(): TimeRange { 197 if (this.explicitlySetZoomRange === undefined) { 198 return this.getFullTimeRange(); 199 } else { 200 return this.explicitlySetZoomRange; 201 } 202 } 203 204 setZoom(zoomRange: TimeRange) { 205 this.explicitlySetZoomRange = zoomRange; 206 } 207 208 getTraces(): Traces { 209 return this.traces; 210 } 211 212 getScreenRecordingVideo(): Blob | undefined { 213 return this.screenRecordingVideo; 214 } 215 216 searchCorrespondingScreenRecordingTimeSeconds( 217 position: TracePosition, 218 ): number | undefined { 219 const trace = this.traces.getTrace(TraceType.SCREEN_RECORDING); 220 if (!trace || trace.lengthEntries === 0) { 221 return undefined; 222 } 223 224 const firstTimestamp = trace.getEntry(0).getTimestamp(); 225 const entry = TraceEntryFinder.findCorrespondingEntry(trace, position); 226 if (!entry) { 227 return undefined; 228 } 229 230 return ScreenRecordingUtils.timestampToVideoTimeSeconds( 231 firstTimestamp.getValueNs(), 232 entry.getTimestamp().getValueNs(), 233 ); 234 } 235 236 hasTimestamps(): boolean { 237 return this.firstEntry !== undefined; 238 } 239 240 hasMoreThanOneDistinctTimestamp(): boolean { 241 return ( 242 this.hasTimestamps() && 243 this.firstEntry?.getTimestamp().getValueNs() !== 244 this.lastEntry?.getTimestamp().getValueNs() 245 ); 246 } 247 248 getPreviousEntryFor(trace: Trace<object>): TraceEntry<object> | undefined { 249 if (trace.lengthEntries === 0) { 250 return undefined; 251 } 252 253 const currentIndex = this.findCurrentEntryFor(trace)?.getIndex(); 254 if (currentIndex === undefined || currentIndex === 0) { 255 return undefined; 256 } 257 258 return trace.getEntry(currentIndex - 1); 259 } 260 261 getNextEntryFor(trace: Trace<object>): TraceEntry<object> | undefined { 262 if (trace.lengthEntries === 0) { 263 return undefined; 264 } 265 266 const currentIndex = this.findCurrentEntryFor(trace)?.getIndex(); 267 if (currentIndex === undefined) { 268 return trace.getEntry(0); 269 } 270 271 if (currentIndex + 1 >= trace.lengthEntries) { 272 return undefined; 273 } 274 275 return trace.getEntry(currentIndex + 1); 276 } 277 278 findCurrentEntryFor(trace: Trace<object>): TraceEntry<object> | undefined { 279 const position = this.getCurrentPosition(); 280 if (!position) { 281 return undefined; 282 } 283 284 const entry = TraceEntryFinder.findCorrespondingEntry(trace, position); 285 286 if ( 287 this.lastReturnedCurrentEntries.get(trace)?.getIndex() !== 288 entry?.getIndex() 289 ) { 290 this.lastReturnedCurrentEntries.set(trace, entry); 291 } 292 293 return this.lastReturnedCurrentEntries.get(trace); 294 } 295 296 moveToPreviousEntryFor(trace: Trace<object>) { 297 const prevEntry = this.getPreviousEntryFor(trace); 298 if (prevEntry !== undefined) { 299 this.setPosition(TracePosition.fromTraceEntry(prevEntry)); 300 } 301 } 302 303 moveToNextEntryFor(trace: Trace<object>) { 304 const nextEntry = this.getNextEntryFor(trace); 305 if (nextEntry !== undefined) { 306 this.setPosition(TracePosition.fromTraceEntry(nextEntry)); 307 } 308 } 309 310 clear() { 311 this.traces = new Traces(); 312 this.firstEntry = undefined; 313 this.lastEntry = undefined; 314 this.explicitlySetPosition = undefined; 315 this.explicitlySetSelection = undefined; 316 this.lastReturnedCurrentPosition = undefined; 317 this.screenRecordingVideo = undefined; 318 this.lastReturnedFullTimeRange = undefined; 319 this.lastReturnedCurrentEntries.clear(); 320 this.activeTrace = undefined; 321 } 322 323 private findFirstEntry(): TraceEntry<{}> | undefined { 324 let first: TraceEntry<{}> | undefined = undefined; 325 326 this.traces.forEachTrace((trace) => { 327 if (trace.lengthEntries === 0) { 328 return; 329 } 330 const candidate = trace.getEntry(0); 331 if (!first || candidate.getTimestamp() < first.getTimestamp()) { 332 first = candidate; 333 } 334 }); 335 336 return first; 337 } 338 339 private findLastEntry(): TraceEntry<{}> | undefined { 340 let last: TraceEntry<{}> | undefined = undefined; 341 342 this.traces.forEachTrace((trace) => { 343 if (trace.lengthEntries === 0) { 344 return; 345 } 346 const candidate = trace.getEntry(trace.lengthEntries - 1); 347 if (!last || candidate.getTimestamp() > last.getTimestamp()) { 348 last = candidate; 349 } 350 }); 351 352 return last; 353 } 354 355 private getFirstEntryOfActiveViewTrace(): TraceEntry<{}> | undefined { 356 if (!this.activeTrace || this.activeTrace.lengthEntries === 0) { 357 return undefined; 358 } 359 return this.activeTrace.getEntry(0); 360 } 361} 362