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