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} from 'common/file_utils'; 18import {OnProgressUpdateType} from 'common/function_utils'; 19import { 20 TimestampConverter, 21 UTC_TIMEZONE_INFO, 22} from 'common/timestamp_converter'; 23import {UserNotifier} from 'common/user_notifier'; 24import {Analytics} from 'logging/analytics'; 25import {ProgressListener} from 'messaging/progress_listener'; 26import {CorruptedArchive, NoValidFiles} from 'messaging/user_warnings'; 27import {FileAndParsers} from 'parsers/file_and_parsers'; 28import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory'; 29import {ParserFactory as PerfettoParserFactory} from 'parsers/perfetto/parser_factory'; 30import {ParserSearch} from 'parsers/search/parser_search'; 31import {TracesParserFactory} from 'parsers/traces/traces_parser_factory'; 32import {FrameMapper} from 'trace/frame_mapper'; 33import {Trace} from 'trace/trace'; 34import {Traces} from 'trace/traces'; 35import {TraceFile} from 'trace/trace_file'; 36import {TraceEntryTypeMap, TraceType, TraceTypeUtils} from 'trace/trace_type'; 37import {QueryResult} from 'trace_processor/query_result'; 38import {FilesSource} from './files_source'; 39import {LoadedParsers} from './loaded_parsers'; 40import {TraceFileFilter} from './trace_file_filter'; 41 42type UnzippedArchive = TraceFile[]; 43 44export class TracePipeline { 45 private loadedParsers = new LoadedParsers(); 46 private traceFileFilter = new TraceFileFilter(); 47 private tracesParserFactory = new TracesParserFactory(); 48 private traces = new Traces(); 49 private downloadArchiveFilename?: string; 50 private timestampConverter = new TimestampConverter(UTC_TIMEZONE_INFO); 51 52 async loadFiles( 53 files: File[], 54 source: FilesSource, 55 progressListener: ProgressListener | undefined, 56 ) { 57 this.downloadArchiveFilename = this.makeDownloadArchiveFilename( 58 files, 59 source, 60 ); 61 62 try { 63 const unzippedArchives = await this.unzipFiles(files, progressListener); 64 65 if (unzippedArchives.length === 0) { 66 UserNotifier.add(new NoValidFiles()); 67 return; 68 } 69 70 for (const unzippedArchive of unzippedArchives) { 71 await this.loadUnzippedArchive(unzippedArchive, progressListener); 72 } 73 74 this.traces = new Traces(); 75 76 this.loadedParsers.getParsers().forEach((parser) => { 77 const trace = Trace.fromParser(parser); 78 this.traces.addTrace(trace); 79 Analytics.Tracing.logTraceLoaded(parser); 80 }); 81 82 const tracesParsers = await this.tracesParserFactory.createParsers( 83 this.traces, 84 this.timestampConverter, 85 ); 86 87 tracesParsers.forEach((tracesParser) => { 88 const trace = Trace.fromParser(tracesParser); 89 this.traces.addTrace(trace); 90 }); 91 92 const hasTransitionTrace = 93 this.traces.getTrace(TraceType.TRANSITION) !== undefined; 94 if (hasTransitionTrace) { 95 this.removeTracesAndParsersByType(TraceType.WM_TRANSITION); 96 this.removeTracesAndParsersByType(TraceType.SHELL_TRANSITION); 97 } 98 99 const hasCujTrace = this.traces.getTrace(TraceType.CUJS) !== undefined; 100 if (hasCujTrace) { 101 this.removeTracesAndParsersByType(TraceType.EVENT_LOG); 102 } 103 104 const hasMergedInputTrace = 105 this.traces.getTrace(TraceType.INPUT_EVENT_MERGED) !== undefined; 106 if (hasMergedInputTrace) { 107 this.removeTracesAndParsersByType(TraceType.INPUT_KEY_EVENT); 108 this.removeTracesAndParsersByType(TraceType.INPUT_MOTION_EVENT); 109 } 110 } finally { 111 progressListener?.onOperationFinished(true); 112 } 113 } 114 115 removeTrace<T extends TraceType>( 116 trace: Trace<TraceEntryTypeMap[T]>, 117 keepFileForDownload = false, 118 ) { 119 this.loadedParsers.remove(trace.getParser(), keepFileForDownload); 120 this.traces.deleteTrace(trace); 121 } 122 123 async makeZipArchiveWithLoadedTraceFiles( 124 onProgressUpdate?: OnProgressUpdateType, 125 ): Promise<Blob> { 126 return this.loadedParsers.makeZipArchive(onProgressUpdate); 127 } 128 129 filterTracesWithoutVisualization() { 130 const tracesWithoutVisualization = this.traces 131 .mapTrace((trace) => { 132 if (!TraceTypeUtils.isTraceTypeWithViewer(trace.type)) { 133 return trace; 134 } 135 return undefined; 136 }) 137 .filter((trace) => trace !== undefined) as Array<Trace<object>>; 138 tracesWithoutVisualization.forEach((trace) => 139 this.traces.deleteTrace(trace), 140 ); 141 } 142 143 async buildTraces() { 144 for (const trace of this.traces) { 145 if (trace.lengthEntries === 0 || trace.isDumpWithoutTimestamp()) { 146 continue; 147 } else { 148 const timestamp = trace.getEntry(0).getTimestamp(); 149 this.timestampConverter.initializeUTCOffset(timestamp); 150 break; 151 } 152 } 153 await new FrameMapper(this.traces).computeMapping(); 154 } 155 156 getTraces(): Traces { 157 return this.traces; 158 } 159 160 getDownloadArchiveFilename(): string { 161 return this.downloadArchiveFilename ?? 'winscope'; 162 } 163 164 getTimestampConverter(): TimestampConverter { 165 return this.timestampConverter; 166 } 167 168 async getScreenRecordingVideo(): Promise<undefined | Blob> { 169 const traces = this.getTraces(); 170 const screenRecording = 171 traces.getTrace(TraceType.SCREEN_RECORDING) ?? 172 traces.getTrace(TraceType.SCREENSHOT); 173 if (!screenRecording || screenRecording.lengthEntries === 0) { 174 return undefined; 175 } 176 return (await screenRecording.getEntry(0).getValue()).videoData; 177 } 178 179 async tryCreateSearchTrace( 180 query: string, 181 ): Promise<Trace<QueryResult> | undefined> { 182 try { 183 const parser = new ParserSearch(query, this.timestampConverter); 184 await parser.parse(); 185 const trace = Trace.fromParser(parser); 186 this.traces.addTrace(trace); 187 return trace; 188 } catch (e) { 189 return undefined; 190 } 191 } 192 193 clear() { 194 this.loadedParsers.clear(); 195 this.traces = new Traces(); 196 this.timestampConverter.clear(); 197 this.downloadArchiveFilename = undefined; 198 } 199 200 private async loadUnzippedArchive( 201 unzippedArchive: UnzippedArchive, 202 progressListener: ProgressListener | undefined, 203 ) { 204 const filterResult = await this.traceFileFilter.filter(unzippedArchive); 205 if (filterResult.timezoneInfo) { 206 this.timestampConverter = new TimestampConverter( 207 filterResult.timezoneInfo, 208 ); 209 } 210 211 if (!filterResult.perfetto && filterResult.legacy.length === 0) { 212 UserNotifier.add(new NoValidFiles()); 213 return; 214 } 215 216 const legacyParsers = await new LegacyParserFactory().createParsers( 217 filterResult.legacy, 218 this.timestampConverter, 219 filterResult.metadata, 220 progressListener, 221 ); 222 223 let perfettoParsers: FileAndParsers | undefined; 224 225 if (filterResult.perfetto) { 226 const parsers = await new PerfettoParserFactory().createParsers( 227 filterResult.perfetto, 228 this.timestampConverter, 229 progressListener, 230 ); 231 perfettoParsers = new FileAndParsers(filterResult.perfetto, parsers); 232 } 233 234 const monotonicTimeOffset = 235 this.loadedParsers.getLatestRealToMonotonicOffset( 236 legacyParsers 237 .map((fileAndParser) => fileAndParser.parser) 238 .concat(perfettoParsers?.parsers ?? []), 239 ); 240 241 const realToBootTimeOffset = 242 this.loadedParsers.getLatestRealToBootTimeOffset( 243 legacyParsers 244 .map((fileAndParser) => fileAndParser.parser) 245 .concat(perfettoParsers?.parsers ?? []), 246 ); 247 248 if (monotonicTimeOffset !== undefined) { 249 this.timestampConverter.setRealToMonotonicTimeOffsetNs( 250 monotonicTimeOffset, 251 ); 252 } 253 if (realToBootTimeOffset !== undefined) { 254 this.timestampConverter.setRealToBootTimeOffsetNs(realToBootTimeOffset); 255 } 256 257 perfettoParsers?.parsers.forEach((p) => p.createTimestamps()); 258 legacyParsers.forEach((fileAndParser) => 259 fileAndParser.parser.createTimestamps(), 260 ); 261 262 this.loadedParsers.addParsers(legacyParsers, perfettoParsers); 263 } 264 265 private makeDownloadArchiveFilename( 266 files: File[], 267 source: FilesSource, 268 ): string { 269 // set download archive file name, used to download all traces 270 let filenameWithCurrTime: string; 271 const currTime = new Date().toISOString().slice(0, -5).replace('T', '_'); 272 if (!this.downloadArchiveFilename && files.length === 1) { 273 const filenameNoDir = FileUtils.removeDirFromFileName(files[0].name); 274 const filenameNoDirOrExt = 275 FileUtils.removeExtensionFromFilename(filenameNoDir); 276 filenameWithCurrTime = `${filenameNoDirOrExt}_${currTime}`; 277 } else { 278 filenameWithCurrTime = `${source}_${currTime}`; 279 } 280 281 const archiveFilenameNoIllegalChars = filenameWithCurrTime.replace( 282 FileUtils.ILLEGAL_FILENAME_CHARACTERS_REGEX, 283 '_', 284 ); 285 if (FileUtils.DOWNLOAD_FILENAME_REGEX.test(archiveFilenameNoIllegalChars)) { 286 return archiveFilenameNoIllegalChars; 287 } else { 288 console.error( 289 "Cannot convert uploaded archive filename to acceptable format for download. Defaulting download filename to 'winscope.zip'.", 290 ); 291 return 'winscope'; 292 } 293 } 294 295 private async unzipFiles( 296 files: File[], 297 progressListener: ProgressListener | undefined, 298 ): Promise<UnzippedArchive[]> { 299 const unzippedArchives: UnzippedArchive[] = []; 300 const progressMessage = 'Unzipping files...'; 301 302 progressListener?.onProgressUpdate(progressMessage, 0); 303 304 const currArchive: UnzippedArchive = []; 305 for (let i = 0; i < files.length; i++) { 306 let file = files[i]; 307 308 const onSubProgressUpdate = (subPercentage: number) => { 309 const totalPercentage = 310 (100 * i) / files.length + subPercentage / files.length; 311 progressListener?.onProgressUpdate(progressMessage, totalPercentage); 312 }; 313 314 if (await FileUtils.isGZipFile(file)) { 315 file = await FileUtils.decompressGZipFile(file); 316 } 317 318 if (await FileUtils.isZipFile(file)) { 319 try { 320 const subFiles = await FileUtils.unzipFile(file, onSubProgressUpdate); 321 const subTraceFiles = subFiles.map((subFile) => { 322 return new TraceFile(subFile, file); 323 }); 324 unzippedArchives.push([...subTraceFiles]); 325 onSubProgressUpdate(100); 326 } catch (e) { 327 UserNotifier.add(new CorruptedArchive(file)); 328 } 329 } else { 330 currArchive.push(new TraceFile(file, undefined)); 331 } 332 } 333 if (currArchive.length > 0) { 334 unzippedArchives.push(currArchive); 335 } 336 progressListener?.onProgressUpdate(progressMessage, 100); 337 338 return unzippedArchives; 339 } 340 341 private removeTracesAndParsersByType(type: TraceType) { 342 const traces = this.traces.getTraces(type); 343 traces.forEach((trace) => { 344 this.removeTrace(trace, true); 345 }); 346 } 347} 348