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 { 18 ChangeDetectorRef, 19 Component, 20 EventEmitter, 21 Inject, 22 Input, 23 OnDestroy, 24 OnInit, 25 Output, 26 ViewEncapsulation, 27} from '@angular/core'; 28import {assertDefined} from 'common/assert_utils'; 29import {PersistentStoreProxy} from 'common/persistent_store_proxy'; 30import {Analytics} from 'logging/analytics'; 31import {ProgressListener} from 'messaging/progress_listener'; 32import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event'; 33import {WinscopeEventListener} from 'messaging/winscope_event_listener'; 34import {Connection} from 'trace_collection/connection'; 35import {ProxyState} from 'trace_collection/proxy_client'; 36import {ProxyConnection} from 'trace_collection/proxy_connection'; 37import { 38 ConfigMap, 39 EnableConfiguration, 40 SelectionConfiguration, 41 TraceConfigurationMap, 42} from 'trace_collection/trace_collection_utils'; 43import {LoadProgressComponent} from './load_progress_component'; 44 45@Component({ 46 selector: 'collect-traces', 47 template: ` 48 <mat-card class="collect-card"> 49 <mat-card-title class="title">Collect Traces</mat-card-title> 50 51 <mat-card-content class="collect-card-content"> 52 <p *ngIf="connect.isConnectingState()" class="connecting-message mat-body-1"> 53 Connecting... 54 </p> 55 56 <div *ngIf="!connect.adbSuccess()" class="set-up-adb"> 57 <button 58 class="proxy-tab" 59 color="primary" 60 mat-stroked-button 61 [ngClass]="tabClass(true)" 62 (click)="displayAdbProxyTab()"> 63 ADB Proxy 64 </button> 65 <!-- <button class="web-tab" color="primary" mat-raised-button [ngClass]="tabClass(false)" (click)="displayWebAdbTab()">Web ADB</button> --> 66 <adb-proxy 67 *ngIf="isAdbProxy" 68 [(proxy)]="connect.proxy" 69 (addKey)="onAddKey($event)"></adb-proxy> 70 <!-- <web-adb *ngIf="!isAdbProxy"></web-adb> TODO: fix web adb workflow --> 71 </div> 72 73 <div *ngIf="connect.isDevicesState()" class="devices-connecting"> 74 <div *ngIf="objectKeys(connect.devices()).length === 0" class="no-device-detected"> 75 <p class="mat-body-3 icon"><mat-icon inline fontIcon="phonelink_erase"></mat-icon></p> 76 <p class="mat-body-1">No devices detected</p> 77 </div> 78 <div *ngIf="objectKeys(connect.devices()).length > 0" class="device-selection"> 79 <p class="mat-body-1 instruction">Select a device:</p> 80 <mat-list *ngIf="objectKeys(connect.devices()).length > 0"> 81 <mat-list-item 82 *ngFor="let deviceId of objectKeys(connect.devices())" 83 (click)="onDeviceClick(deviceId)" 84 class="available-device"> 85 <mat-icon matListIcon> 86 {{ 87 connect.devices()[deviceId].authorised ? 'smartphone' : 'screen_lock_portrait' 88 }} 89 </mat-icon> 90 <p matLine> 91 {{ 92 connect.devices()[deviceId].authorised 93 ? connect.devices()[deviceId]?.model 94 : 'unauthorised' 95 }} 96 ({{ deviceId }}) 97 </p> 98 </mat-list-item> 99 </mat-list> 100 </div> 101 </div> 102 103 <div 104 *ngIf="showTraceCollectionConfig()" 105 class="trace-collection-config"> 106 <mat-list> 107 <mat-list-item> 108 <mat-icon matListIcon>smartphone</mat-icon> 109 <p matLine> 110 {{ connect.selectedDevice()?.model }} ({{ connect.selectedDeviceId() }}) 111 112 <button 113 color="primary" 114 class="change-btn" 115 mat-button 116 (click)="onChangeDeviceButton()" 117 [disabled]="connect.isEndTraceState() || isOperationInProgress()"> 118 Change device 119 </button> 120 </p> 121 </mat-list-item> 122 </mat-list> 123 124 <mat-tab-group [selectedIndex]="selectedTabIndex" class="tracing-tabs"> 125 <mat-tab 126 label="Trace" 127 [disabled]="connect.isEndTraceState() || isOperationInProgress() || refreshDumps"> 128 <div class="tabbed-section"> 129 <div class="trace-section" *ngIf="connect.isStartTraceState()"> 130 <trace-config [(traceConfig)]="traceConfig"></trace-config> 131 <div class="start-btn"> 132 <button color="primary" mat-stroked-button (click)="startTracing()"> 133 Start trace 134 </button> 135 </div> 136 </div> 137 138 <div *ngIf="connect.isStartingTraceState()" class="starting-trace"> 139 <load-progress 140 message="Starting trace..."> 141 </load-progress> 142 <div class="end-btn"> 143 <button color="primary" mat-raised-button [disabled]="true"> 144 End trace 145 </button> 146 </div> 147 </div> 148 149 <div *ngIf="connect.isEndTraceState()" class="end-tracing"> 150 <load-progress 151 icon="cable" 152 message="Tracing..."> 153 </load-progress> 154 <div class="end-btn"> 155 <button color="primary" mat-raised-button (click)="endTrace()"> 156 End trace 157 </button> 158 </div> 159 </div> 160 161 <div *ngIf="isOperationInProgress()" class="load-data"> 162 <load-progress 163 [progressPercentage]="progressPercentage" 164 [message]="progressMessage"> 165 </load-progress> 166 <div class="end-btn"> 167 <button color="primary" mat-raised-button [disabled]="true"> 168 End trace 169 </button> 170 </div> 171 </div> 172 </div> 173 </mat-tab> 174 <mat-tab label="Dump" [disabled]="connect.isEndTraceState() || isOperationInProgress()"> 175 <div class="tabbed-section"> 176 <div class="dump-section" *ngIf="connect.isStartTraceState() && !refreshDumps"> 177 <h3 class="mat-subheading-2">Dump targets</h3> 178 <div class="selection"> 179 <mat-checkbox 180 *ngFor="let dumpKey of objectKeys(dumpConfig)" 181 color="primary" 182 class="dump-checkbox" 183 [(ngModel)]="dumpConfig[dumpKey].run" 184 >{{ dumpConfig[dumpKey].name }}</mat-checkbox 185 > 186 </div> 187 <div class="dump-btn" *ngIf="!refreshDumps"> 188 <button color="primary" mat-stroked-button (click)="dumpState()"> 189 Dump state 190 </button> 191 </div> 192 </div> 193 194 <load-progress 195 *ngIf="refreshDumps || isOperationInProgress()" 196 [progressPercentage]="progressPercentage" 197 [message]="progressMessage"> 198 </load-progress> 199 </div> 200 </mat-tab> 201 </mat-tab-group> 202 </div> 203 204 <div *ngIf="connect.isErrorState()" class="unknown-error"> 205 <p class="error-wrapper mat-body-1"> 206 <mat-icon class="error-icon">error</mat-icon> 207 Error: 208 </p> 209 <pre> {{ connect.proxy?.errorText }} </pre> 210 <button color="primary" class="retry-btn" mat-raised-button (click)="onRetryButton()"> 211 Retry 212 </button> 213 </div> 214 </mat-card-content> 215 </mat-card> 216 `, 217 styles: [ 218 ` 219 .change-btn, 220 .retry-btn { 221 margin-left: 5px; 222 } 223 .mat-card.collect-card { 224 display: flex; 225 } 226 .collect-card { 227 height: 100%; 228 flex-direction: column; 229 overflow: auto; 230 margin: 10px; 231 } 232 .collect-card-content { 233 overflow: auto; 234 } 235 .selection { 236 display: flex; 237 flex-direction: row; 238 flex-wrap: wrap; 239 gap: 10px; 240 } 241 .set-up-adb, 242 .trace-collection-config, 243 .trace-section, 244 .dump-section, 245 .starting-trace, 246 .end-tracing, 247 .load-data, 248 trace-config { 249 display: flex; 250 flex-direction: column; 251 gap: 10px; 252 } 253 .trace-section, 254 .dump-section, 255 .starting-trace, 256 .end-tracing, 257 .load-data { 258 height: 100%; 259 } 260 .trace-collection-config { 261 height: 100%; 262 } 263 .proxy-tab, 264 .web-tab, 265 .start-btn, 266 .dump-btn, 267 .end-btn { 268 align-self: flex-start; 269 } 270 .start-btn, 271 .dump-btn, 272 .end-btn { 273 margin: auto 0 0 0; 274 padding: 1rem 0 0 0; 275 } 276 .error-wrapper { 277 display: flex; 278 flex-direction: row; 279 align-items: center; 280 } 281 .error-icon { 282 margin-right: 5px; 283 } 284 .available-device { 285 cursor: pointer; 286 } 287 288 .no-device-detected { 289 display: flex; 290 flex-direction: column; 291 justify-content: center; 292 align-content: center; 293 align-items: center; 294 height: 100%; 295 } 296 297 .no-device-detected p, 298 .device-selection p.instruction { 299 padding-top: 1rem; 300 opacity: 0.6; 301 font-size: 1.2rem; 302 } 303 304 .no-device-detected .icon { 305 font-size: 3rem; 306 margin: 0 0 0.2rem 0; 307 } 308 309 .devices-connecting { 310 height: 100%; 311 } 312 313 mat-card-content { 314 flex-grow: 1; 315 } 316 317 mat-tab-body { 318 padding: 1rem; 319 } 320 321 .loading-info { 322 opacity: 0.8; 323 padding: 1rem 0; 324 } 325 326 .tracing-tabs { 327 flex-grow: 1; 328 } 329 330 .tracing-tabs .mat-tab-body-wrapper { 331 flex-grow: 1; 332 } 333 334 .tabbed-section { 335 height: 100%; 336 } 337 338 .progress-desc { 339 display: flex; 340 height: 100%; 341 flex-direction: column; 342 justify-content: center; 343 align-content: center; 344 align-items: center; 345 } 346 347 .progress-desc > * { 348 max-width: 250px; 349 } 350 351 load-progress { 352 height: 100%; 353 } 354 `, 355 ], 356 encapsulation: ViewEncapsulation.None, 357}) 358export class CollectTracesComponent 359 implements OnInit, OnDestroy, ProgressListener, WinscopeEventListener 360{ 361 objectKeys = Object.keys; 362 isAdbProxy = true; 363 connect: Connection | undefined; 364 isExternalOperationInProgress = false; 365 progressMessage = 'Fetching...'; 366 progressPercentage: number | undefined; 367 lastUiProgressUpdateTimeMs?: number; 368 refreshDumps = false; 369 selectedTabIndex = 0; 370 371 @Input() traceConfig: TraceConfigurationMap | undefined; 372 @Input() dumpConfig: TraceConfigurationMap | undefined; 373 @Input() storage: Storage | undefined; 374 375 @Output() readonly filesCollected = new EventEmitter<File[]>(); 376 377 constructor( 378 @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, 379 ) {} 380 381 ngOnInit() { 382 if (this.isAdbProxy) { 383 this.connect = new ProxyConnection( 384 (newState) => this.onProxyStateChange(), 385 (progress) => this.onLoadProgressUpdate(progress), 386 this.setTraceConfigForAvailableTraces, 387 ); 388 } else { 389 // TODO: change to WebAdbConnection 390 this.connect = new ProxyConnection( 391 (newState) => this.onProxyStateChange(), 392 (progress) => this.onLoadProgressUpdate(progress), 393 this.setTraceConfigForAvailableTraces, 394 ); 395 } 396 } 397 398 ngOnDestroy(): void { 399 assertDefined(this.connect).proxy?.removeOnProxyChange(this.onProxyChange); 400 } 401 402 async onDeviceClick(deviceId: string) { 403 await assertDefined(this.connect).selectDevice(deviceId); 404 } 405 406 async onWinscopeEvent(event: WinscopeEvent) { 407 await event.visit( 408 WinscopeEventType.APP_REFRESH_DUMPS_REQUEST, 409 async (event) => { 410 this.selectedTabIndex = 1; 411 this.progressMessage = 'Refreshing dumps...'; 412 this.progressPercentage = 0; 413 this.refreshDumps = true; 414 }, 415 ); 416 } 417 418 onProgressUpdate(message: string, progressPercentage: number | undefined) { 419 if ( 420 !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs) 421 ) { 422 return; 423 } 424 this.isExternalOperationInProgress = true; 425 this.progressMessage = message; 426 this.progressPercentage = progressPercentage; 427 this.lastUiProgressUpdateTimeMs = Date.now(); 428 this.changeDetectorRef.detectChanges(); 429 } 430 431 onOperationFinished() { 432 this.isExternalOperationInProgress = false; 433 this.lastUiProgressUpdateTimeMs = undefined; 434 this.changeDetectorRef.detectChanges(); 435 } 436 437 isOperationInProgress(): boolean { 438 return ( 439 assertDefined(this.connect).isLoadDataState() || 440 this.isExternalOperationInProgress 441 ); 442 } 443 444 async onAddKey(key: string) { 445 if (this.connect?.setProxyKey) { 446 this.connect.setProxyKey(key); 447 } 448 await assertDefined(this.connect).restart(); 449 } 450 451 displayAdbProxyTab() { 452 this.isAdbProxy = true; 453 this.connect = new ProxyConnection( 454 (newState) => this.onProxyStateChange(), 455 (progress) => this.onLoadProgressUpdate(progress), 456 this.setTraceConfigForAvailableTraces, 457 ); 458 } 459 460 displayWebAdbTab() { 461 this.isAdbProxy = false; 462 //TODO: change to WebAdbConnection 463 this.connect = new ProxyConnection( 464 (newState) => this.onProxyStateChange(), 465 (progress) => this.onLoadProgressUpdate(progress), 466 this.setTraceConfigForAvailableTraces, 467 ); 468 } 469 470 showTraceCollectionConfig() { 471 const connect = assertDefined(this.connect); 472 return ( 473 connect.isStartTraceState() || 474 connect.isStartingTraceState() || 475 connect.isEndTraceState() || 476 this.isOperationInProgress() 477 ); 478 } 479 480 async onChangeDeviceButton() { 481 await assertDefined(this.connect).resetLastDevice(); 482 } 483 484 async onRetryButton() { 485 await assertDefined(this.connect).restart(); 486 } 487 488 async startTracing() { 489 console.log('begin tracing'); 490 const requestedTraces = this.getRequestedTraces(); 491 Analytics.Tracing.logCollectTraces(requestedTraces); 492 const reqEnableConfig = this.requestedEnableConfig(); 493 const reqSelectedSfConfig = this.requestedSelection('layers_trace'); 494 const reqSelectedWmConfig = this.requestedSelection('window_trace'); 495 if (requestedTraces.length < 1) { 496 await assertDefined(this.connect).throwNoTargetsError(); 497 return; 498 } 499 500 await assertDefined(this.connect).startTrace( 501 requestedTraces, 502 reqEnableConfig, 503 reqSelectedSfConfig, 504 reqSelectedWmConfig, 505 ); 506 } 507 508 async dumpState() { 509 console.log('begin dump'); 510 const requestedDumps = this.getRequestedDumps(); 511 Analytics.Tracing.logCollectDumps(requestedDumps); 512 const dumpSuccessful = await assertDefined(this.connect).dumpState( 513 requestedDumps, 514 ); 515 this.refreshDumps = false; 516 if (dumpSuccessful) { 517 this.filesCollected.emit(assertDefined(this.connect).adbData()); 518 } 519 } 520 521 async endTrace() { 522 console.log('end tracing'); 523 await assertDefined(this.connect).endTrace(); 524 this.filesCollected.emit(assertDefined(this.connect).adbData()); 525 } 526 527 tabClass(adbTab: boolean) { 528 let isActive: string; 529 if (adbTab) { 530 isActive = this.isAdbProxy ? 'active' : 'inactive'; 531 } else { 532 isActive = !this.isAdbProxy ? 'active' : 'inactive'; 533 } 534 return ['tab', isActive]; 535 } 536 537 private async onProxyChange(newState: ProxyState) { 538 await assertDefined(this.connect).onConnectChange.bind(this.connect)( 539 newState, 540 ); 541 } 542 543 private onProxyStateChange() { 544 this.changeDetectorRef.detectChanges(); 545 if ( 546 !this.refreshDumps || 547 this.connect?.isLoadDataState() || 548 this.connect?.isConnectingState() 549 ) { 550 return; 551 } 552 if (this.connect?.isStartTraceState()) { 553 this.dumpState(); 554 } else { 555 // device is not connected or proxy is not started/invalid/in error state 556 // so cannot refresh dump automatically 557 this.refreshDumps = false; 558 } 559 } 560 561 private getRequestedTraces() { 562 const tracesFromCollection: string[] = []; 563 const tracingConfig = assertDefined(this.traceConfig); 564 const requested = Object.keys(tracingConfig).filter((traceKey: string) => { 565 return tracingConfig[traceKey].run; 566 }); 567 requested.push(...tracesFromCollection); 568 requested.push('perfetto_trace'); // always start/stop/fetch perfetto trace 569 return requested; 570 } 571 572 private getRequestedDumps() { 573 const dumpConfig = assertDefined(this.dumpConfig); 574 const requested = Object.keys(dumpConfig).filter((dumpKey: string) => { 575 return dumpConfig[dumpKey].run; 576 }); 577 requested.push('perfetto_dump'); // always dump/fetch perfetto dump 578 return requested; 579 } 580 581 private requestedEnableConfig(): string[] { 582 const req: string[] = []; 583 const tracingConfig = assertDefined(this.traceConfig); 584 Object.keys(tracingConfig).forEach((traceKey: string) => { 585 const trace = tracingConfig[traceKey]; 586 if (trace.run && trace.config && trace.config.enableConfigs) { 587 trace.config.enableConfigs.forEach((con: EnableConfiguration) => { 588 if (con.enabled) { 589 req.push(con.key); 590 } 591 }); 592 } 593 }); 594 return req; 595 } 596 597 private requestedSelection(traceType: string): ConfigMap | undefined { 598 const tracingConfig = assertDefined(this.traceConfig); 599 if (!tracingConfig[traceType].run) { 600 return undefined; 601 } 602 const selected: ConfigMap = {}; 603 tracingConfig[traceType].config?.selectionConfigs.forEach( 604 (con: SelectionConfiguration) => { 605 selected[con.key] = con.value; 606 }, 607 ); 608 return selected; 609 } 610 611 private onLoadProgressUpdate(progressPercentage: number) { 612 this.progressPercentage = progressPercentage; 613 this.changeDetectorRef.detectChanges(); 614 } 615 616 private setTraceConfigForAvailableTraces = ( 617 availableTracesConfig: TraceConfigurationMap, 618 ) => 619 (this.traceConfig = PersistentStoreProxy.new<TraceConfigurationMap>( 620 'TraceConfiguration', 621 availableTracesConfig, 622 assertDefined(this.storage), 623 )); 624} 625