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 {FileUtils, OnFile} from 'common/file_utils'; 18import {BuganizerAttachmentsDownloadEmitter} from 'interfaces/buganizer_attachments_download_emitter'; 19import {ProgressListener} from 'interfaces/progress_listener'; 20import {RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver'; 21import {RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver'; 22import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender'; 23import {Runnable} from 'interfaces/runnable'; 24import {TraceDataListener} from 'interfaces/trace_data_listener'; 25import {TracePositionUpdateEmitter} from 'interfaces/trace_position_update_emitter'; 26import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener'; 27import {UserNotificationListener} from 'interfaces/user_notification_listener'; 28import {Timestamp, TimestampType} from 'trace/timestamp'; 29import {TraceFile} from 'trace/trace_file'; 30import {TracePosition} from 'trace/trace_position'; 31import {TraceType} from 'trace/trace_type'; 32import {View, Viewer} from 'viewers/viewer'; 33import {ViewerFactory} from 'viewers/viewer_factory'; 34import {TimelineData} from './timeline_data'; 35import {TracePipeline} from './trace_pipeline'; 36 37type TimelineComponentInterface = TracePositionUpdateListener & TracePositionUpdateEmitter; 38type CrossToolProtocolInterface = RemoteBugreportReceiver & 39 RemoteTimestampReceiver & 40 RemoteTimestampSender; 41type AbtChromeExtensionProtocolInterface = BuganizerAttachmentsDownloadEmitter & Runnable; 42 43export class Mediator { 44 private abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface; 45 private crossToolProtocol: CrossToolProtocolInterface; 46 private uploadTracesComponent?: ProgressListener; 47 private collectTracesComponent?: ProgressListener; 48 private timelineComponent?: TimelineComponentInterface; 49 private appComponent: TraceDataListener; 50 private userNotificationListener: UserNotificationListener; 51 private storage: Storage; 52 53 private tracePipeline: TracePipeline; 54 private timelineData: TimelineData; 55 private viewers: Viewer[] = []; 56 private isChangingCurrentTimestamp = false; 57 private isTraceDataVisualized = false; 58 private lastRemoteToolTimestampReceived: Timestamp | undefined; 59 private currentProgressListener?: ProgressListener; 60 61 constructor( 62 tracePipeline: TracePipeline, 63 timelineData: TimelineData, 64 abtChromeExtensionProtocol: AbtChromeExtensionProtocolInterface, 65 crossToolProtocol: CrossToolProtocolInterface, 66 appComponent: TraceDataListener, 67 userNotificationListener: UserNotificationListener, 68 storage: Storage 69 ) { 70 this.tracePipeline = tracePipeline; 71 this.timelineData = timelineData; 72 this.abtChromeExtensionProtocol = abtChromeExtensionProtocol; 73 this.crossToolProtocol = crossToolProtocol; 74 this.appComponent = appComponent; 75 this.userNotificationListener = userNotificationListener; 76 this.storage = storage; 77 78 this.crossToolProtocol.setOnBugreportReceived( 79 async (bugreport: File, timestamp?: Timestamp) => { 80 await this.onRemoteBugreportReceived(bugreport, timestamp); 81 } 82 ); 83 84 this.crossToolProtocol.setOnTimestampReceived(async (timestamp: Timestamp) => { 85 await this.onRemoteTimestampReceived(timestamp); 86 }); 87 88 this.abtChromeExtensionProtocol.setOnBuganizerAttachmentsDownloadStart(() => { 89 this.onBuganizerAttachmentsDownloadStart(); 90 }); 91 92 this.abtChromeExtensionProtocol.setOnBuganizerAttachmentsDownloaded( 93 async (attachments: File[]) => { 94 await this.onBuganizerAttachmentsDownloaded(attachments); 95 } 96 ); 97 } 98 99 setUploadTracesComponent(uploadTracesComponent: ProgressListener | undefined) { 100 this.uploadTracesComponent = uploadTracesComponent; 101 } 102 103 setCollectTracesComponent(collectTracesComponent: ProgressListener | undefined) { 104 this.collectTracesComponent = collectTracesComponent; 105 } 106 107 setTimelineComponent(timelineComponent: TimelineComponentInterface | undefined) { 108 this.timelineComponent = timelineComponent; 109 this.timelineComponent?.setOnTracePositionUpdate(async (position) => { 110 await this.onTimelineTracePositionUpdate(position); 111 }); 112 } 113 114 onWinscopeInitialized() { 115 this.abtChromeExtensionProtocol.run(); 116 } 117 118 onWinscopeUploadNew() { 119 this.resetAppToInitialState(); 120 } 121 122 async onWinscopeFilesUploaded(files: File[]) { 123 this.currentProgressListener = this.uploadTracesComponent; 124 await this.processFiles(files); 125 } 126 127 async onWinscopeFilesCollected(files: File[]) { 128 this.currentProgressListener = this.collectTracesComponent; 129 await this.processFiles(files); 130 await this.processLoadedTraceFiles(); 131 } 132 133 async onWinscopeViewTracesRequest() { 134 await this.processLoadedTraceFiles(); 135 } 136 137 async onWinscopeActiveViewChanged(view: View) { 138 this.timelineData.setActiveViewTraceTypes(view.dependencies); 139 await this.propagateTracePosition(this.timelineData.getCurrentPosition()); 140 } 141 142 async onTimelineTracePositionUpdate(position: TracePosition) { 143 await this.propagateTracePosition(position); 144 } 145 146 private async propagateTracePosition( 147 position?: TracePosition, 148 omitCrossToolProtocol: boolean = false 149 ) { 150 if (!position) { 151 return; 152 } 153 154 //TODO (b/289478304): update only visible viewers (1 tab viewer + overlay viewers) 155 const promises = this.viewers.map((viewer) => { 156 return viewer.onTracePositionUpdate(position); 157 }); 158 await Promise.all(promises); 159 160 this.timelineComponent?.onTracePositionUpdate(position); 161 162 if (omitCrossToolProtocol) { 163 return; 164 } 165 166 const timestamp = position.timestamp; 167 if (timestamp.getType() !== TimestampType.REAL) { 168 console.warn( 169 'Cannot propagate timestamp change to remote tool.' + 170 ` Remote tool expects timestamp type ${TimestampType.REAL},` + 171 ` but Winscope wants to notify timestamp type ${timestamp.getType()}.` 172 ); 173 return; 174 } 175 176 this.crossToolProtocol.sendTimestamp(timestamp); 177 } 178 179 private onBuganizerAttachmentsDownloadStart() { 180 this.resetAppToInitialState(); 181 this.currentProgressListener = this.uploadTracesComponent; 182 this.currentProgressListener?.onProgressUpdate('Downloading files...', undefined); 183 } 184 185 private async onBuganizerAttachmentsDownloaded(attachments: File[]) { 186 this.currentProgressListener = this.uploadTracesComponent; 187 await this.processRemoteFilesReceived(attachments); 188 } 189 190 private async onRemoteBugreportReceived(bugreport: File, timestamp?: Timestamp) { 191 this.currentProgressListener = this.uploadTracesComponent; 192 await this.processRemoteFilesReceived([bugreport]); 193 if (timestamp !== undefined) { 194 await this.onRemoteTimestampReceived(timestamp); 195 } 196 } 197 198 private async onRemoteTimestampReceived(timestamp: Timestamp) { 199 this.lastRemoteToolTimestampReceived = timestamp; 200 201 if (!this.isTraceDataVisualized) { 202 return; // apply timestamp later when traces are visualized 203 } 204 205 if (this.timelineData.getTimestampType() !== timestamp.getType()) { 206 console.warn( 207 'Cannot apply new timestamp received from remote tool.' + 208 ` Remote tool notified timestamp type ${timestamp.getType()},` + 209 ` but Winscope is accepting timestamp type ${this.timelineData.getTimestampType()}.` 210 ); 211 return; 212 } 213 214 const position = TracePosition.fromTimestamp(timestamp); 215 this.timelineData.setPosition(position); 216 217 await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true); 218 } 219 220 private async processRemoteFilesReceived(files: File[]) { 221 this.resetAppToInitialState(); 222 await this.processFiles(files); 223 } 224 225 private async processFiles(files: File[]) { 226 let progressMessage = ''; 227 const onProgressUpdate = (progressPercentage: number) => { 228 this.currentProgressListener?.onProgressUpdate(progressMessage, progressPercentage); 229 }; 230 231 const traceFiles: TraceFile[] = []; 232 const onFile: OnFile = (file: File, parentArchive?: File) => { 233 traceFiles.push(new TraceFile(file, parentArchive)); 234 }; 235 236 progressMessage = 'Unzipping files...'; 237 this.currentProgressListener?.onProgressUpdate(progressMessage, 0); 238 await FileUtils.unzipFilesIfNeeded(files, onFile, onProgressUpdate); 239 240 progressMessage = 'Parsing files...'; 241 this.currentProgressListener?.onProgressUpdate(progressMessage, 0); 242 const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate); 243 this.currentProgressListener?.onOperationFinished(); 244 this.userNotificationListener?.onParserErrors(parserErrors); 245 } 246 247 private async processLoadedTraceFiles() { 248 this.currentProgressListener?.onProgressUpdate('Computing frame mapping...', undefined); 249 250 // allow the UI to update before making the main thread very busy 251 await new Promise<void>((resolve) => setTimeout(resolve, 10)); 252 253 await this.tracePipeline.buildTraces(); 254 this.currentProgressListener?.onOperationFinished(); 255 256 this.timelineData.initialize( 257 this.tracePipeline.getTraces(), 258 await this.tracePipeline.getScreenRecordingVideo() 259 ); 260 await this.createViewers(); 261 this.appComponent.onTraceDataLoaded(this.viewers); 262 this.isTraceDataVisualized = true; 263 264 if (this.lastRemoteToolTimestampReceived !== undefined) { 265 await this.onRemoteTimestampReceived(this.lastRemoteToolTimestampReceived); 266 } 267 } 268 269 private async createViewers() { 270 const traces = this.tracePipeline.getTraces(); 271 const traceTypes = new Set<TraceType>(); 272 traces.forEachTrace((trace) => { 273 traceTypes.add(trace.type); 274 }); 275 this.viewers = new ViewerFactory().createViewers(traceTypes, traces, this.storage); 276 277 // Set position as soon as the viewers are created 278 await this.propagateTracePosition(this.timelineData.getCurrentPosition(), true); 279 } 280 281 private async executeIgnoringRecursiveTimestampNotifications(op: () => Promise<void>) { 282 if (this.isChangingCurrentTimestamp) { 283 return; 284 } 285 this.isChangingCurrentTimestamp = true; 286 try { 287 await op(); 288 } finally { 289 this.isChangingCurrentTimestamp = false; 290 } 291 } 292 293 private resetAppToInitialState() { 294 this.tracePipeline.clear(); 295 this.timelineData.clear(); 296 this.viewers = []; 297 this.isTraceDataVisualized = false; 298 this.lastRemoteToolTimestampReceived = undefined; 299 this.appComponent.onTraceDataUnloaded(); 300 } 301} 302