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