1/* 2 * Copyright (C) 2025 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 {assertDefined} from 'common/assert_utils'; 18import {AdbFileIdentifier, TraceTarget} from 'trace_collection/trace_target'; 19import {UiTraceTarget} from 'trace_collection/ui/ui_trace_target'; 20import {UserRequest, UserRequestConfig} from 'trace_collection/user_request'; 21import {PerfettoSessionModerator} from './perfetto_session_moderator'; 22import {TracingSession} from './tracing_session'; 23 24const WINSCOPE_EXT = '.winscope'; 25const WINSCOPE_EXT_LEGACY = '.pb'; 26const WINSCOPE_EXTS = [WINSCOPE_EXT, WINSCOPE_EXT_LEGACY]; 27const WINSCOPE_DIR = '/data/misc/wmtrace/'; 28 29function makeMatchersWithWinscopeExts(matcher: string) { 30 return WINSCOPE_EXTS.map((ext) => `${matcher}${ext}`); 31} 32 33export class UserRequestParser { 34 private readonly targetPerfettoDsMap = new Map([ 35 [UiTraceTarget.SURFACE_FLINGER_TRACE, 'android.surfaceflinger.layers'], 36 [UiTraceTarget.WINDOW_MANAGER_TRACE, 'android.windowmanager'], 37 [UiTraceTarget.IME, 'android.inputmethod'], 38 [UiTraceTarget.TRANSACTIONS, 'android.surfaceflinger.transactions'], 39 [UiTraceTarget.PROTO_LOG, 'android.protolog'], 40 [UiTraceTarget.TRANSITIONS, 'com.android.wm.shell.transition'], 41 [UiTraceTarget.VIEW_CAPTURE, 'android.viewcapture'], 42 [UiTraceTarget.INPUT, 'android.input.inputevent'], 43 [UiTraceTarget.SURFACE_FLINGER_DUMP, 'android.surfaceflinger.layers'], 44 ]); 45 46 private perfettoModerator: PerfettoSessionModerator | undefined; 47 private requests: UserRequest[] | undefined; 48 49 setPerfettoModerator(value: PerfettoSessionModerator) { 50 this.perfettoModerator = value; 51 return this; 52 } 53 54 setRequests(value: UserRequest[]) { 55 this.requests = value; 56 return this; 57 } 58 59 async parse(): Promise<TracingSession[]> { 60 const traceTargets: TraceTarget[] = []; 61 const perfettoSetup: string[] = []; 62 const perfettoModerator = assertDefined(this.perfettoModerator); 63 64 for (const req of assertDefined(this.requests)) { 65 const ds = this.targetPerfettoDsMap.get(req.target); 66 const dataSourceAvailable = 67 ds !== undefined && (await perfettoModerator.isDataSourceAvailable(ds)); 68 69 const isPerfetto = 70 !(await perfettoModerator.isTooManySessions()) && dataSourceAvailable; 71 72 if (isPerfetto) { 73 const cmd = this.getPerfettoSetupCommand(req); 74 if (cmd) { 75 perfettoSetup.push(cmd); 76 } 77 } else { 78 const targets = this.getNonPerfettoTargets(req); 79 if (targets) { 80 traceTargets.push(...targets); 81 } 82 } 83 } 84 85 const sessions = traceTargets.map((target) => { 86 return new TracingSession(target); 87 }); 88 if (perfettoSetup.length > 0) { 89 sessions.push(perfettoModerator.createTracingSession(perfettoSetup)); 90 } 91 return sessions; 92 } 93 94 private getPerfettoSetupCommand(req: UserRequest): string | undefined { 95 switch (req.target) { 96 case UiTraceTarget.SURFACE_FLINGER_TRACE: 97 return this.getSfTracePerfettoSetupCommand(req); 98 case UiTraceTarget.WINDOW_MANAGER_TRACE: 99 return this.getWmTracePerfettoSetupCommand(req); 100 case UiTraceTarget.VIEW_CAPTURE: 101 return this.getVcPerfettoSetupCommand(); 102 case UiTraceTarget.TRANSACTIONS: 103 return this.getTransactionsPerfettoSetupCommand(); 104 case UiTraceTarget.PROTO_LOG: 105 return this.getProtologPerfettoSetupCommand(); 106 case UiTraceTarget.IME: 107 return this.getImePerfettoSetupCommand(); 108 case UiTraceTarget.TRANSITIONS: 109 return this.getTransitionsPerfettoSetupCommand(); 110 case UiTraceTarget.INPUT: 111 return this.getInputPerfettoSetupCommand(); 112 case UiTraceTarget.SURFACE_FLINGER_DUMP: 113 return this.getSfDumpPerfettoSetupCommand(); 114 default: 115 return undefined; 116 } 117 } 118 119 private getNonPerfettoTargets(req: UserRequest) { 120 switch (req.target) { 121 case UiTraceTarget.SURFACE_FLINGER_TRACE: 122 return [this.getSfTraceLegacyTarget(req)]; 123 case UiTraceTarget.WINDOW_MANAGER_TRACE: 124 return [this.getWmTraceLegacyTarget(req)]; 125 case UiTraceTarget.VIEW_CAPTURE: 126 return [this.getVcLegacyTarget()]; 127 case UiTraceTarget.TRANSACTIONS: 128 return [this.getTransactionsLegacyTarget()]; 129 case UiTraceTarget.PROTO_LOG: 130 return [this.getProtologLegacyTarget()]; 131 case UiTraceTarget.IME: 132 return [this.getImeLegacyTarget()]; 133 case UiTraceTarget.TRANSITIONS: 134 return [this.getTransitionsLegacyTarget()]; 135 case UiTraceTarget.SCREEN_RECORDING: 136 return this.getScreenRecordingTargets(req); 137 case UiTraceTarget.WAYLAND: 138 return [this.getWaylandTarget()]; 139 case UiTraceTarget.EVENTLOG: 140 return [this.getEventlogTarget()]; 141 case UiTraceTarget.SURFACE_FLINGER_DUMP: 142 return [this.getSfDumpLegacyTarget()]; 143 case UiTraceTarget.WINDOW_MANAGER_DUMP: 144 return [this.getWmDumpLegacyTarget()]; 145 case UiTraceTarget.SCREENSHOT: 146 return this.getScreenshotTargets(req); 147 default: 148 return undefined; 149 } 150 } 151 152 private getSfTracePerfettoSetupCommand(req: UserRequest) { 153 const flagsMap: {[key: string]: string} = { 154 'input': 'TRACE_FLAG_INPUT', 155 'composition': 'TRACE_FLAG_COMPOSITION', 156 'metadata': 'TRACE_FLAG_EXTRA', 157 'hwc': 'TRACE_FLAG_HWC', 158 'tracebuffers': 'TRACE_FLAG_BUFFERS', 159 'virtualdisplays': 'TRACE_FLAG_VIRTUAL_DISPLAYS', 160 }; 161 const {flags} = new SfRequestConfigParser(flagsMap).parse(req.config); 162 163 const spacer = '\n '; 164 const flagsCmd = flags 165 .map((flag: string) => { 166 return `trace_flags: ${flagsMap[flag]}`; 167 }) 168 .join(spacer); 169 return this.perfettoModerator?.createSetupCommand( 170 'android.surfaceflinger.layers', 171 `surfaceflinger_layers_config: { 172 mode: MODE_ACTIVE${flagsCmd.length === 0 ? '' : spacer + flagsCmd} 173 }`, 174 ); 175 } 176 177 private getSfTraceLegacyTarget(req: UserRequest) { 178 const flagsMap: {[key: string]: number} = { 179 'input': 1 << 1, 180 'composition': 1 << 2, 181 'metadata': 1 << 3, 182 'hwc': 1 << 4, 183 'tracebuffers': 1 << 5, 184 'virtualdisplays': 1 << 6, 185 }; 186 const {flags, selectedConfigs} = new SfRequestConfigParser(flagsMap).parse( 187 req.config, 188 ); 189 let flagsValue = 0; 190 for (const flag of flags) { 191 flagsValue |= flagsMap[flag]; 192 } 193 const setupCommands = [ 194 `su root service call SurfaceFlinger 1029 i32 ${selectedConfigs['sfbuffersize']}$`, 195 `su root service call SurfaceFlinger 1033 i32 ${flagsValue}`, 196 ]; 197 198 return new TraceTarget( 199 'SfLegacyTrace', 200 setupCommands, 201 'su root service call SurfaceFlinger 1025 i32 1' + 202 '\necho "SF layers trace (legacy) started."', 203 'su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1' + 204 '\necho "SF layers trace (legacy) stopped."', 205 [ 206 new AdbFileIdentifier( 207 WINSCOPE_DIR, 208 makeMatchersWithWinscopeExts('layers_trace'), 209 'layers_trace', 210 ), 211 ], 212 ); 213 } 214 215 private getWmTracePerfettoSetupCommand(req: UserRequest) { 216 const selectedConfigs = new WmRequestConfigParser().parse(req.config); 217 218 const logLevelMap: {[key: string]: string} = { 219 'verbose': 'LOG_LEVEL_VERBOSE', 220 'debug': 'LOG_LEVEL_DEBUG', 221 'critical': 'LOG_LEVEL_CRITICAL', 222 }; 223 224 const frequencyMap: {[key: string]: string} = { 225 'frame': 'LOG_FREQUENCY_FRAME', 226 'transaction': 'LOG_FREQUENCY_TRANSACTION', 227 }; 228 229 const logLevel = logLevelMap[selectedConfigs['tracinglevel']]; 230 const logFrequency = frequencyMap[selectedConfigs['tracingtype']]; 231 return this.perfettoModerator?.createSetupCommand( 232 'android.windowmanager', 233 `windowmanager_config: { 234 log_level: ${logLevel} 235 log_frequency: ${logFrequency} 236 }`, 237 ); 238 } 239 240 private getWmTraceLegacyTarget(req: UserRequest) { 241 const selectedConfigs = new WmRequestConfigParser().parse(req.config); 242 243 const setupCmds = [ 244 `su root cmd window tracing ${selectedConfigs['tracingtype']}`, 245 `su root cmd window tracing level ${selectedConfigs['tracinglevel']}`, 246 `su root cmd window tracing size ${selectedConfigs['wmbuffersize']}`, 247 ]; 248 249 return new TraceTarget( 250 'WmLegacyTrace', 251 setupCmds, 252 'su root cmd window tracing start' + 253 '\necho "WM trace (legacy) started."', 254 'su root cmd window tracing stop' + '\necho "WM trace (legacy) stopped."', 255 [ 256 new AdbFileIdentifier( 257 WINSCOPE_DIR, 258 makeMatchersWithWinscopeExts('wm_trace'), 259 'window_trace', 260 ), 261 ], 262 ); 263 } 264 265 private getVcPerfettoSetupCommand() { 266 return this.perfettoModerator?.createSetupCommand('android.viewcapture'); 267 } 268 269 private getVcLegacyTarget() { 270 return new TraceTarget( 271 'VcLegacy', 272 [], 273 'su root settings put global view_capture_enabled 1' + 274 '\necho "ViewCapture tracing (legacy) started."', 275 'su root sh -c "cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip"' + 276 '\nsu root settings put global view_capture_enabled 0' + 277 '\necho "ViewCapture tracing (legacy) stopped."', 278 [ 279 new AdbFileIdentifier( 280 WINSCOPE_DIR, 281 ['view_capture_trace.zip'], 282 'view_capture_trace.zip', 283 ), 284 ], 285 ); 286 } 287 288 private getTransactionsPerfettoSetupCommand() { 289 return this.perfettoModerator?.createSetupCommand( 290 'android.surfaceflinger.transactions', 291 `surfaceflinger_transactions_config: { 292 mode: MODE_ACTIVE 293 }`, 294 ); 295 } 296 297 private getTransactionsLegacyTarget() { 298 return new TraceTarget( 299 'TransactionsLegacy', 300 [], 301 'su root service call SurfaceFlinger 1041 i32 1' + 302 '\necho "SF transactions trace (legacy) started."', 303 'su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1' + 304 '\necho "SF transactions trace (legacy) stopped."', 305 [ 306 new AdbFileIdentifier( 307 WINSCOPE_DIR, 308 makeMatchersWithWinscopeExts('transactions_trace'), 309 'transactions', 310 ), 311 ], 312 ); 313 } 314 315 private getProtologPerfettoSetupCommand() { 316 return this.perfettoModerator?.createSetupCommand( 317 'android.protolog', 318 `protolog_config: { 319 tracing_mode: ENABLE_ALL 320 }`, 321 ); 322 } 323 324 private getProtologLegacyTarget() { 325 return new TraceTarget( 326 'ProtologLegacy', 327 [], 328 'su root cmd window logging start' + 329 '\necho "ProtoLog (legacy) started."', 330 'su root cmd window logging stop >/dev/null 2>&1' + 331 '\necho "ProtoLog (legacy) stopped."', 332 [ 333 new AdbFileIdentifier( 334 WINSCOPE_DIR, 335 makeMatchersWithWinscopeExts('wm_log'), 336 'proto_log', 337 ), 338 ], 339 ); 340 } 341 342 private getImePerfettoSetupCommand() { 343 return this.perfettoModerator?.createSetupCommand('android.inputmethod'); 344 } 345 346 private getImeLegacyTarget() { 347 return new TraceTarget( 348 'ImeLegacy', 349 [], 350 'su root ime tracing start\necho "IME tracing (legacy) started."', 351 'su root ime tracing stop >/dev/null 2>&1\necho "IME tracing (legacy) stopped."', 352 [ 353 new AdbFileIdentifier( 354 WINSCOPE_DIR, 355 makeMatchersWithWinscopeExts('ime_trace_clients'), 356 'ime_trace_clients', 357 ), 358 new AdbFileIdentifier( 359 WINSCOPE_DIR, 360 makeMatchersWithWinscopeExts('ime_trace_service'), 361 'ime_trace_service', 362 ), 363 new AdbFileIdentifier( 364 WINSCOPE_DIR, 365 makeMatchersWithWinscopeExts('ime_trace_managerservice'), 366 'ime_trace_managerservice', 367 ), 368 ], 369 ); 370 } 371 372 private getTransitionsPerfettoSetupCommand() { 373 return this.perfettoModerator?.createSetupCommand( 374 'com.android.wm.shell.transition', 375 ); 376 } 377 378 private getTransitionsLegacyTarget() { 379 return new TraceTarget( 380 'TransitionsLegacy', 381 [], 382 'su root cmd window shell tracing start ' + 383 '&& su root dumpsys activity service SystemUIService WMShell transitions tracing start' + 384 '\necho "Transition traces (legacy) started."', 385 'su root cmd window shell tracing stop ' + 386 '&& su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1' + 387 '\n echo "Transition traces (legacy) stopped."', 388 [ 389 new AdbFileIdentifier( 390 WINSCOPE_DIR, 391 makeMatchersWithWinscopeExts('wm_transition_trace'), 392 'wm_transition_trace', 393 ), 394 new AdbFileIdentifier( 395 WINSCOPE_DIR, 396 makeMatchersWithWinscopeExts('shell_transition_trace'), 397 'shell_transition_trace', 398 ), 399 ], 400 ); 401 } 402 403 private getInputPerfettoSetupCommand() { 404 return this.perfettoModerator?.createSetupCommand( 405 'android.input.inputevent', 406 `android_input_event_config { 407 mode: TRACE_MODE_TRACE_ALL 408 }`, 409 ); 410 } 411 412 private getScreenRecordingTargets(req: UserRequest) { 413 const {identifiers, showPointerAndTouches} = 414 new ScreenRecordingConfigParser().parse(req.config); 415 416 const val = showPointerAndTouches ? '1' : '0'; 417 const setupCmd = `settings put system show_touches ${val} && settings put system pointer_location ${val}`; 418 const stopCmd = `settings put system pointer_location 0 && \ 419 settings put system show_touches 0 && \ 420 pkill -l SIGINT screenrecord >/dev/null 2>&1`; 421 422 return identifiers.map((id, index) => { 423 const startArgs = id === 'active' ? '' : ` --display-id ${id}`; 424 const startCmd = ` 425 screenrecord --bugreport --bit-rate 8M${startArgs} /data/local/tmp/screen_${id}.mp4 & \ 426 echo "ScreenRecorder started." 427 `; 428 429 return new TraceTarget( 430 'ScreenRecording' + id, 431 index === 0 ? [setupCmd] : [], 432 startCmd, 433 stopCmd, 434 [ 435 new AdbFileIdentifier( 436 `/data/local/tmp/screen_${id}.mp4`, 437 [], 438 `screen_recording_${id}`, 439 ), 440 ], 441 true, 442 ); 443 }); 444 } 445 446 private getScreenshotTargets(req: UserRequest) { 447 const identifiers = new ScreenshotConfigParser().parse(req.config); 448 449 return identifiers.map((id) => { 450 const startArgs = id === 'active' ? '' : ` -d ${id}`; 451 const startCmd = `screencap -p${startArgs} > /data/local/tmp/screenshot_${id}.png`; 452 453 return new TraceTarget('Screenshot' + id, [], startCmd, '', [ 454 new AdbFileIdentifier( 455 `/data/local/tmp/screenshot_${id}.png`, 456 [], 457 `screenshot_${id}.png`, 458 ), 459 ]); 460 }); 461 } 462 463 private getWaylandTarget() { 464 return new TraceTarget( 465 'Wayland', 466 [], 467 'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."', 468 'su root service call Wayland 26 i32 0 >/dev/null\necho "Wayland trace ended."', 469 [ 470 new AdbFileIdentifier( 471 '/data/misc/wltrace', 472 makeMatchersWithWinscopeExts('wl_trace'), 473 'wl_trace', 474 ), 475 ], 476 ); 477 } 478 479 private getEventlogTarget() { 480 const startTimeSeconds = (Date.now() / 1000).toString(); 481 return new TraceTarget( 482 'Eventlog', 483 [], 484 'rm -f /data/local/tmp/eventlog.winscope' + '\n echo "EventLog started."', 485 'echo "EventLog\\n" > /data/local/tmp/eventlog.winscope ' + 486 `&& su root logcat -b events -v threadtime -v printable -v uid -v nsec -v epoch -b events -t ${startTimeSeconds} >> /data/local/tmp/eventlog.winscope`, 487 [ 488 new AdbFileIdentifier( 489 '/data/local/tmp', 490 makeMatchersWithWinscopeExts('eventlog'), 491 'eventlog', 492 ), 493 ], 494 ); 495 } 496 497 private getSfDumpPerfettoSetupCommand() { 498 return this.perfettoModerator?.createSetupCommand( 499 'android.surfaceflinger.layers', 500 `surfaceflinger_layers_config: { 501 mode: MODE_DUMP 502 trace_flags: TRACE_FLAG_INPUT 503 trace_flags: TRACE_FLAG_COMPOSITION 504 trace_flags: TRACE_FLAG_HWC 505 trace_flags: TRACE_FLAG_BUFFERS 506 trace_flags: TRACE_FLAG_VIRTUAL_DISPLAYS 507 }`, 508 ); 509 } 510 511 private getSfDumpLegacyTarget() { 512 return new TraceTarget( 513 'SfDumpLegacy', 514 [], 515 `su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump${WINSCOPE_EXT}`, 516 '', 517 [ 518 new AdbFileIdentifier( 519 `/data/local/tmp/sf_dump${WINSCOPE_EXT}`, 520 [], 521 'layers_dump', 522 ), 523 ], 524 ); 525 } 526 527 private getWmDumpLegacyTarget() { 528 return new TraceTarget( 529 'WmDumpLegacy', 530 [], 531 `su root dumpsys window --proto > /data/local/tmp/wm_dump${WINSCOPE_EXT}`, 532 '', 533 [ 534 new AdbFileIdentifier( 535 `/data/local/tmp/wm_dump${WINSCOPE_EXT}`, 536 [], 537 'window_dump', 538 ), 539 ], 540 ); 541 } 542} 543 544class SfRequestConfigParser { 545 private readonly configs: {[key: string]: string} = { 546 'sfbuffersize': '16000', 547 }; 548 549 constructor(private readonly flagsMap: object) {} 550 551 parse(req: UserRequestConfig[]) { 552 const flags: string[] = []; 553 req.forEach((config) => { 554 if (config.key in this.flagsMap) { 555 flags.push(config.key); 556 } else if ( 557 config.key in this.configs && 558 typeof config.value === 'string' 559 ) { 560 this.configs[config.key] = config.value ?? ''; 561 } 562 }); 563 return {flags, selectedConfigs: this.configs}; 564 } 565} 566 567class WmRequestConfigParser { 568 private readonly configs: {[key: string]: string} = { 569 'wmbuffersize': '16000', 570 'tracinglevel': 'debug', 571 'tracingtype': 'frame', 572 }; 573 574 parse(req: UserRequestConfig[]) { 575 req.forEach((config) => { 576 if (config.key in this.configs && typeof config.value === 'string') { 577 this.configs[config.key] = config.value ?? this.configs[config.key]; 578 } 579 }); 580 return this.configs; 581 } 582} 583 584abstract class MediaBasedConfigParser { 585 protected getIdentifiers(req: UserRequestConfig[]): string[] { 586 const identifiers = ['active']; 587 const config = req.find((c) => c.key === 'displays'); 588 if (config?.value) { 589 if (typeof config.value === 'string') { 590 const display = this.parseDisplayId(config.value); 591 if (display) return [display]; 592 } else { 593 const displays = config.value.map((v) => this.parseDisplayId(v)); 594 if (displays.length > 0) return displays; 595 } 596 } 597 return identifiers; 598 } 599 600 private parseDisplayId(displayValue: string): string { 601 // display value comes in form '"<displayName>" <displayId> <otherInfo>' 602 // where '"<displayName>"' is optional 603 if (displayValue[0] === '"') { 604 displayValue = displayValue.split('"')[2].trim(); 605 } 606 return displayValue.split(' ')[0]; 607 } 608} 609 610class ScreenshotConfigParser extends MediaBasedConfigParser { 611 parse(req: UserRequestConfig[]) { 612 return this.getIdentifiers(req); 613 } 614} 615 616class ScreenRecordingConfigParser extends MediaBasedConfigParser { 617 parse(req: UserRequestConfig[]) { 618 return { 619 identifiers: this.getIdentifiers(req), 620 showPointerAndTouches: req.find((c) => c.key === 'pointer_and_touches'), 621 }; 622 } 623} 624