Remove tracing config singleton and improve change detection.
New flow:
Trace and dump config initialized in CollectTracesComponent
-> passed into respective TraceConfigComponents
-> updated in TraceConfigComponent
-> propagated back into CollectTracesComponent
-> sent to ProxyConnection on trace/dump start request
The above does not fix change detection issues. There appears to be some bug with change detection when checkboxes/mat select are in ngIf blocks that keep changing.
Added a worker that manually detects changes every 0.1s.
Also fixed wayland trace availability request (was does not firing due to selectedDevice check, but does not need selected device to run).
Fix: 300384655
Test: npm run test:unit:ci
Change-Id: I215e4b4a473bd806d426b31b9a03f6618d931148
diff --git a/tools/winscope/src/app/components/app_component.ts b/tools/winscope/src/app/components/app_component.ts
index 84b8e65..2821db4 100644
--- a/tools/winscope/src/app/components/app_component.ts
+++ b/tools/winscope/src/app/components/app_component.ts
@@ -35,7 +35,6 @@
import {globalConfig} from 'common/global_config';
import {InMemoryStorage} from 'common/in_memory_storage';
import {PersistentStore} from 'common/persistent_store';
-import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {Timestamp} from 'common/time';
import {UrlUtils} from 'common/url_utils';
import {UserNotifier} from 'common/user_notifier';
@@ -55,10 +54,6 @@
import {WinscopeEventListener} from 'messaging/winscope_event_listener';
import {AdbConnection} from 'trace_collection/adb_connection';
import {ProxyConnection} from 'trace_collection/proxy_connection';
-import {
- TraceConfigurationMap,
- TRACES,
-} from 'trace_collection/trace_collection_utils';
import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles';
import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component';
import {Viewer} from 'viewers/viewer';
@@ -225,8 +220,6 @@
<div class="card-grid landing-grid">
<collect-traces
class="collect-traces-card homepage-card"
- [traceConfig]="traceConfig"
- [dumpConfig]="dumpConfig"
[storage]="traceConfigStorage"
[adbConnection]="adbConnection"
(filesCollected)="onFilesCollected($event)"></collect-traces>
@@ -354,8 +347,6 @@
]),
);
adbConnection: AdbConnection = new ProxyConnection();
- traceConfig: TraceConfigurationMap;
- dumpConfig: TraceConfigurationMap;
traceConfigStorage: Storage;
@ViewChild(UploadTracesComponent)
@@ -460,33 +451,6 @@
this.traceConfigStorage =
globalConfig.MODE === 'PROD' ? localStorage : new InMemoryStorage();
- this.traceConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
- 'TracingSettings',
- TRACES['default'],
- this.traceConfigStorage,
- );
- this.dumpConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
- 'DumpSettings',
- {
- window_dump: {
- name: 'Window Manager',
- run: true,
- config: undefined,
- },
- layers_dump: {
- name: 'Surface Flinger',
- run: true,
- config: undefined,
- },
- screenshot: {
- name: 'Screenshot',
- run: true,
- config: undefined,
- },
- },
- this.traceConfigStorage,
- );
-
window.onunhandledrejection = (evt) => {
Analytics.Error.logGlobalException(evt.reason);
};
diff --git a/tools/winscope/src/app/components/collect_traces_component.ts b/tools/winscope/src/app/components/collect_traces_component.ts
index 0a6c835..cf64241 100644
--- a/tools/winscope/src/app/components/collect_traces_component.ts
+++ b/tools/winscope/src/app/components/collect_traces_component.ts
@@ -27,7 +27,6 @@
import {MatDialog} from '@angular/material/dialog';
import {assertDefined} from 'common/assert_utils';
import {FunctionUtils} from 'common/function_utils';
-import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {Analytics} from 'logging/analytics';
import {ProgressListener} from 'messaging/progress_listener';
import {
@@ -46,9 +45,11 @@
import {ProxyConnection} from 'trace_collection/proxy_connection';
import {
EnableConfiguration,
+ makeDefaultDumpConfigMap,
+ makeDefaultTraceConfigMap,
SelectionConfiguration,
TraceConfigurationMap,
-} from 'trace_collection/trace_collection_utils';
+} from 'trace_collection/trace_configuration';
import {TraceRequest, TraceRequestConfig} from 'trace_collection/trace_request';
import {LoadProgressComponent} from './load_progress_component';
import {
@@ -152,7 +153,12 @@
[disabled]="disableTraceSection()">
<div class="tabbed-section">
<div class="trace-section" *ngIf="adbConnection.getState() === ${ConnectionState.IDLE}">
- <trace-config [traceConfig]="traceConfig"></trace-config>
+ <trace-config
+ title="Trace targets"
+ [initialTraceConfig]="traceConfig"
+ [storage]="storage"
+ traceConfigStoreKey="TraceSettings"
+ (traceConfigChange)="onTraceConfigChange($event)"></trace-config>
<div class="start-btn">
<button color="primary" mat-raised-button (click)="startTracing()">
Start trace
@@ -211,15 +217,12 @@
<mat-tab label="Dump" [disabled]="isTracingOrLoading()">
<div class="tabbed-section">
<div class="dump-section" *ngIf="adbConnection.getState() === ${ConnectionState.IDLE} && !refreshDumps">
- <h3 class="mat-subheading-2">Dump targets</h3>
- <div class="selection">
- <mat-checkbox
- *ngFor="let dumpKey of objectKeys(dumpConfig)"
- color="primary"
- class="dump-checkbox"
- [(ngModel)]="dumpConfig[dumpKey].run"
- >{{ dumpConfig[dumpKey].name }}</mat-checkbox>
- </div>
+ <trace-config
+ title="Dump targets"
+ [initialTraceConfig]="dumpConfig"
+ [storage]="storage"
+ traceConfigStoreKey="DumpSettings"
+ (traceConfigChange)="onDumpConfigChange($event)"></trace-config>
<div class="dump-btn" *ngIf="!refreshDumps">
<button color="primary" mat-raised-button (click)="dumpState()">
Dump state
@@ -411,6 +414,8 @@
lastUiProgressUpdateTimeMs?: number;
refreshDumps = false;
selectedTabIndex = 0;
+ traceConfig: TraceConfigurationMap;
+ dumpConfig: TraceConfigurationMap;
private readonly storeKeyImeWarning = 'doNotShowImeWarningDialog';
private readonly storeKeyLastDevice = 'adb.lastDevice';
@@ -425,8 +430,6 @@
];
@Input() adbConnection: AdbConnection | undefined;
- @Input() traceConfig: TraceConfigurationMap | undefined;
- @Input() dumpConfig: TraceConfigurationMap | undefined;
@Input() storage: Storage | undefined;
@Output() readonly filesCollected = new EventEmitter<File[]>();
@@ -435,7 +438,10 @@
@Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
@Inject(MatDialog) private dialog: MatDialog,
@Inject(NgZone) private ngZone: NgZone,
- ) {}
+ ) {
+ this.traceConfig = makeDefaultTraceConfigMap();
+ this.dumpConfig = makeDefaultDumpConfigMap();
+ }
ngOnChanges() {
if (!this.adbConnection) {
@@ -444,7 +450,7 @@
this.adbConnection.initialize(
() => this.onConnectionStateChange(),
(progress) => this.onLoadProgressUpdate(progress),
- this.setTraceConfigForAvailableTraces,
+ this.toggleAvailabilityOfTraces,
);
}
@@ -547,6 +553,14 @@
);
}
+ onTraceConfigChange(newConfig: TraceConfigurationMap) {
+ this.traceConfig = newConfig;
+ }
+
+ onDumpConfigChange(newConfig: TraceConfigurationMap) {
+ this.dumpConfig = newConfig;
+ }
+
async onChangeDeviceButton() {
this.storage?.setItem(this.storeKeyLastDevice, '');
this.selectedDevice = undefined;
@@ -598,14 +612,16 @@
data,
disableClose: true,
});
- dialogRef.beforeClosed().subscribe((result: WarningDialogResult) => {
- if (this.storage && result.selectedOptions.includes(optionText)) {
- this.storage.setItem(this.storeKeyImeWarning, 'true');
- }
- if (result.closeActionText === closeText) {
- this.requestTraces(requestedTraces);
- }
- });
+ dialogRef
+ .beforeClosed()
+ .subscribe((result: WarningDialogResult | undefined) => {
+ if (this.storage && result?.selectedOptions.includes(optionText)) {
+ this.storage.setItem(this.storeKeyImeWarning, 'true');
+ }
+ if (result?.closeActionText === closeText) {
+ this.requestTraces(requestedTraces);
+ }
+ });
});
}
@@ -748,21 +764,21 @@
private getRequestedTraces(): string[] {
const tracingConfig = assertDefined(this.traceConfig);
return Object.keys(tracingConfig).filter((traceKey: string) => {
- return tracingConfig[traceKey].run;
+ return tracingConfig[traceKey].enabled;
});
}
private getRequestedDumps(): string[] {
const dumpConfig = assertDefined(this.dumpConfig);
return Object.keys(dumpConfig).filter((dumpKey: string) => {
- return dumpConfig[dumpKey].run;
+ return dumpConfig[dumpKey].enabled;
});
}
private requestedEnabledConfig(traceName: string): TraceRequestConfig[] {
const req: TraceRequestConfig[] = [];
const trace = assertDefined(this.traceConfig)[traceName];
- if (trace?.run) {
+ if (trace?.enabled) {
trace.config?.enableConfigs?.forEach((con: EnableConfiguration) => {
if (con.enabled) {
req.push({key: con.key});
@@ -775,7 +791,7 @@
private requestedSelectedConfig(traceName: string): TraceRequestConfig[] {
const tracingConfig = assertDefined(this.traceConfig);
const trace = tracingConfig[traceName];
- if (!trace?.run) {
+ if (!trace?.enabled) {
return [];
}
return (
@@ -790,12 +806,9 @@
this.changeDetectorRef.detectChanges();
}
- private setTraceConfigForAvailableTraces = (
- availableTracesConfig: TraceConfigurationMap,
- ) =>
- (this.traceConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
- 'TraceConfiguration',
- availableTracesConfig,
- assertDefined(this.storage),
- ));
+ private toggleAvailabilityOfTraces = (traces: string[]) =>
+ traces.forEach((trace) => {
+ const config = assertDefined(this.traceConfig)[trace];
+ config.available = !config.available;
+ });
}
diff --git a/tools/winscope/src/app/components/collect_traces_component_test.ts b/tools/winscope/src/app/components/collect_traces_component_test.ts
index c310bb2..56810f9 100644
--- a/tools/winscope/src/app/components/collect_traces_component_test.ts
+++ b/tools/winscope/src/app/components/collect_traces_component_test.ts
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import {CommonModule} from '@angular/common';
import {Component, NO_ERRORS_SCHEMA, ViewChild} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FormsModule} from '@angular/forms';
@@ -28,17 +29,12 @@
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {assertDefined} from 'common/assert_utils';
import {InMemoryStorage} from 'common/in_memory_storage';
-import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {NoTraceTargetsSelected, WinscopeEvent} from 'messaging/winscope_event';
import {MockAdbConnection} from 'test/unit/mock_adb_connection';
import {AdbConnection} from 'trace_collection/adb_connection';
import {AdbDevice} from 'trace_collection/adb_device';
import {ConnectionState} from 'trace_collection/connection_state';
import {ProxyConnection} from 'trace_collection/proxy_connection';
-import {
- TraceConfigurationMap,
- TRACES,
-} from 'trace_collection/trace_collection_utils';
import {AdbProxyComponent} from './adb_proxy_component';
import {CollectTracesComponent} from './collect_traces_component';
import {LoadProgressComponent} from './load_progress_component';
@@ -59,6 +55,7 @@
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
+ CommonModule,
MatIconModule,
MatCardModule,
MatListModule,
@@ -241,11 +238,61 @@
expect(el.innerHTML).toContain('Pixel 6');
expect(el.innerHTML).toContain('35562');
- const traceSection = htmlElement.querySelector('.trace-section');
- expect(traceSection).toBeTruthy();
+ const traceSection = assertDefined(
+ htmlElement.querySelector('.trace-section'),
+ );
+ expect(traceSection.querySelector('trace-config')?.textContent).toContain(
+ 'Trace targets',
+ );
+ expect(traceSection.querySelector('.start-btn')?.textContent).toContain(
+ 'Start trace',
+ );
- const dumpSection = htmlElement.querySelector('.dump-section');
- expect(dumpSection).toBeTruthy();
+ const dumpSection = assertDefined(
+ htmlElement.querySelector('.dump-section'),
+ );
+ expect(dumpSection.querySelector('trace-config')?.textContent).toContain(
+ 'Dump targets',
+ );
+ expect(dumpSection.querySelector('.dump-btn')?.textContent).toContain(
+ 'Dump state',
+ );
+ });
+
+ it('updates config on change in trace config component', async () => {
+ goToConfigSection();
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ expect(
+ component.collectTracesComponent?.traceConfig['window_trace']?.enabled,
+ ).toBeTrue();
+ const traceSection = assertDefined(
+ htmlElement.querySelector('.trace-section'),
+ );
+ const traceCheckboxInput = assertDefined(
+ traceSection.querySelector<HTMLInputElement>('.trace-checkbox input'),
+ );
+ traceCheckboxInput.click();
+ fixture.detectChanges();
+ expect(
+ component.collectTracesComponent?.traceConfig['window_trace']?.enabled,
+ ).toBeFalse();
+
+ expect(
+ component.collectTracesComponent?.dumpConfig['window_dump']?.enabled,
+ ).toBeTrue();
+ const dumpSection = assertDefined(
+ htmlElement.querySelector('.dump-section'),
+ );
+ const dumpCheckboxInput = assertDefined(
+ dumpSection.querySelector<HTMLInputElement>('.trace-checkbox input'),
+ );
+ dumpCheckboxInput.click();
+ fixture.detectChanges();
+ expect(
+ component.collectTracesComponent?.dumpConfig['window_dump']?.enabled,
+ ).toBeFalse();
});
it('start trace button works as expected', async () => {
@@ -264,9 +311,9 @@
lastEvent = event;
});
- Object.values(assertDefined(component.traceConfig)).forEach(
- (c) => (c.run = false),
- );
+ Object.values(
+ assertDefined(component.collectTracesComponent?.traceConfig),
+ ).forEach((c) => (c.enabled = false));
const spy = spyOn(getConnection(), 'startTrace');
await clickStartTraceButton();
@@ -291,9 +338,9 @@
lastEvent = event;
});
- Object.values(assertDefined(component.dumpConfig)).forEach(
- (c) => (c.run = false),
- );
+ Object.values(
+ assertDefined(component.collectTracesComponent?.dumpConfig),
+ ).forEach((c) => (c.enabled = false));
const filesSpy = spyOn(getCollectTracesComponent().filesCollected, 'emit');
await clickDumpStateButton();
@@ -546,6 +593,17 @@
expect(spy).toHaveBeenCalled();
});
+ it('update available traces from connection', () => {
+ expect(
+ component.collectTracesComponent?.traceConfig['wayland_trace']?.available,
+ ).toBeFalse();
+ getConnection().availableTracesChangeCallback(['wayland_trace']);
+ fixture.detectChanges();
+ expect(
+ component.collectTracesComponent?.traceConfig['wayland_trace']?.available,
+ ).toBeTrue();
+ });
+
describe('ProxyConnection', () => {
beforeEach(async () => {
await startProxyConnection();
@@ -606,9 +664,9 @@
}
function updateTraceConfigToInvalidIMEFrameMapping() {
- const config = assertDefined(component.traceConfig);
- config['ime'].run = true;
- config['layers_trace'].run = false;
+ const config = assertDefined(component.collectTracesComponent?.traceConfig);
+ config['ime'].enabled = true;
+ config['layers_trace'].enabled = false;
}
async function clickStartTraceButton() {
@@ -656,35 +714,12 @@
template: `
<collect-traces
[adbConnection]="adbConnection"
- [storage]="storage"
- [traceConfig]="traceConfig"
- [dumpConfig]="dumpConfig"></collect-traces>
+ [storage]="storage"></collect-traces>
`,
})
class TestHostComponent {
adbConnection: AdbConnection = new MockAdbConnection();
storage = new InMemoryStorage();
- traceConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
- 'TracingSettings',
- TRACES['default'],
- this.storage,
- );
- dumpConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
- 'DumpSettings',
- {
- window_dump: {
- name: 'Window Manager',
- run: true,
- config: undefined,
- },
- layers_dump: {
- name: 'Surface Flinger',
- run: true,
- config: undefined,
- },
- },
- this.storage,
- );
@ViewChild(CollectTracesComponent)
collectTracesComponent: CollectTracesComponent | undefined;
diff --git a/tools/winscope/src/app/components/trace_config_component.ts b/tools/winscope/src/app/components/trace_config_component.ts
index 385737c..3dc9b36 100644
--- a/tools/winscope/src/app/components/trace_config_component.ts
+++ b/tools/winscope/src/app/components/trace_config_component.ts
@@ -13,27 +13,38 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {Component, Input} from '@angular/core';
+import {
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ Inject,
+ Input,
+ Output,
+} from '@angular/core';
+import {MatSelect, MatSelectChange} from '@angular/material/select';
import {assertDefined} from 'common/assert_utils';
+import {globalConfig} from 'common/global_config';
+import {PersistentStoreProxy} from 'common/persistent_store_proxy';
import {
EnableConfiguration,
SelectionConfiguration,
TraceConfiguration,
TraceConfigurationMap,
-} from 'trace_collection/trace_collection_utils';
+} from 'trace_collection/trace_configuration';
@Component({
selector: 'trace-config',
template: `
- <h3 class="mat-subheading-2">Trace targets</h3>
+ <h3 class="mat-subheading-2">{{title}}</h3>
<div class="checkboxes">
<mat-checkbox
*ngFor="let traceKey of objectKeys(this.traceConfig)"
color="primary"
class="trace-checkbox"
- [checked]="this.traceConfig[traceKey].run"
- (change)="changeRunTrace($event.checked, this.traceConfig[traceKey])"
+ [disabled]="!this.traceConfig[traceKey].available"
+ [(ngModel)]="this.traceConfig[traceKey].enabled"
+ (ngModelChange)="onTraceConfigChange()"
>{{ this.traceConfig[traceKey].name }}</mat-checkbox
>
</div>
@@ -50,8 +61,9 @@
*ngFor="let enableConfig of traceEnableConfigs(this.traceConfig[traceKey])"
color="primary"
class="enable-config"
- [disabled]="!this.traceConfig[traceKey].run"
+ [disabled]="!this.traceConfig[traceKey].enabled"
[(ngModel)]="enableConfig.enabled"
+ (ngModelChange)="onTraceConfigChange()"
>{{ enableConfig.name }}</mat-checkbox
>
</div>
@@ -59,21 +71,26 @@
<div
*ngIf="this.traceConfig[traceKey].config?.selectionConfigs.length > 0"
class="selection-config-opt">
- <mat-form-field
- *ngFor="let selectionConfig of traceSelectionConfigs(this.traceConfig[traceKey])"
- class="config-selection"
- appearance="fill">
- <mat-label>{{ selectionConfig.name }}</mat-label>
+ <ng-container *ngFor="let selectionConfig of traceSelectionConfigs(this.traceConfig[traceKey])">
+ <mat-form-field
+ class="config-selection"
+ appearance="fill">
+ <mat-label>{{ selectionConfig.name }}</mat-label>
- <mat-select
- class="selected-value"
- [(value)]="selectionConfig.value"
- [disabled]="!this.traceConfig[traceKey].run">
- <mat-option *ngFor="let option of selectionConfig.options" value="{{ option }}">{{
- option
- }}</mat-option>
- </mat-select>
- </mat-form-field>
+ <mat-select
+ #matSelect
+ disableOptionCentering
+ class="selected-value"
+ [attr.label]="traceKey + selectionConfig.name"
+ [value]="selectionConfig.value"
+ [disabled]="!this.traceConfig[traceKey].enabled"
+ (selectionChange)="onSelectChange($event, selectionConfig)">
+ <mat-option *ngFor="let option of selectionConfig.options" (click)="onOptionClick(matSelect, option, traceKey + selectionConfig.name)" [value]="option">{{
+ option
+ }}</mat-option>
+ </mat-select>
+ </mat-form-field>
+ </ng-container>
</div>
</ng-container>
`,
@@ -91,13 +108,50 @@
flex-wrap: wrap;
gap: 10px;
}
+ .config-panel {
+ position: absolute;
+ left: 0px;
+ top: 100px;
+ }
`,
],
})
export class TraceConfigComponent {
objectKeys = Object.keys;
+ changeDetectionWorker: number | undefined;
+ traceConfig: TraceConfigurationMap | undefined;
- @Input() traceConfig: TraceConfigurationMap | undefined;
+ @Input() title: string | undefined;
+ @Input() traceConfigStoreKey: string | undefined;
+ @Input() initialTraceConfig: TraceConfigurationMap | undefined;
+ @Input() storage: Storage | undefined;
+ @Output() readonly traceConfigChange =
+ new EventEmitter<TraceConfigurationMap>();
+
+ constructor(
+ @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
+ ) {}
+
+ ngOnInit() {
+ this.traceConfig = PersistentStoreProxy.new<TraceConfigurationMap>(
+ assertDefined(this.traceConfigStoreKey),
+ assertDefined(
+ this.initialTraceConfig,
+ () => 'component initialized without config',
+ ),
+ assertDefined(this.storage),
+ );
+ if (globalConfig.MODE !== 'KARMA_TEST') {
+ this.changeDetectionWorker = window.setInterval(
+ () => this.changeDetectorRef.detectChanges(),
+ 200,
+ );
+ }
+ }
+
+ ngOnDestroy() {
+ window.clearInterval(this.changeDetectionWorker);
+ }
advancedConfigTraces() {
const advancedConfigs: string[] = [];
@@ -127,12 +181,27 @@
someTraces(trace: TraceConfiguration): boolean {
return (
- !trace.run &&
+ !trace.enabled &&
this.traceEnableConfigs(trace).filter((trace) => trace.enabled).length > 0
);
}
- changeRunTrace(run: boolean, trace: TraceConfiguration): void {
- trace.run = run;
+ onSelectChange(event: MatSelectChange, config: SelectionConfiguration) {
+ config.value = event.value;
+ event.source.close();
+ this.onTraceConfigChange();
+ }
+
+ onOptionClick(select: MatSelect, option: string, configName: string) {
+ if (select.value === option) {
+ const selectElement = assertDefined(
+ document.querySelector(`mat-select[label="${configName}"]`),
+ );
+ (selectElement as HTMLElement).blur();
+ }
+ }
+
+ onTraceConfigChange() {
+ this.traceConfigChange.emit(this.traceConfig);
}
}
diff --git a/tools/winscope/src/app/components/trace_config_component_test.ts b/tools/winscope/src/app/components/trace_config_component_test.ts
index 5eacb54..66dcfb1 100644
--- a/tools/winscope/src/app/components/trace_config_component_test.ts
+++ b/tools/winscope/src/app/components/trace_config_component_test.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import {CommonModule} from '@angular/common';
-import {NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
@@ -23,12 +23,14 @@
import {MatSelectModule} from '@angular/material/select';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {assertDefined} from 'common/assert_utils';
+import {InMemoryStorage} from 'common/in_memory_storage';
import {TraceConfigComponent} from './trace_config_component';
describe('TraceConfigComponent', () => {
let fixture: ComponentFixture<TraceConfigComponent>;
let component: TraceConfigComponent;
let htmlElement: HTMLElement;
+ let configChangeSpy: jasmine.Spy;
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -40,17 +42,22 @@
MatInputModule,
MatSelectModule,
BrowserAnimationsModule,
+ FormsModule,
+ ReactiveFormsModule,
],
declarations: [TraceConfigComponent],
- schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(TraceConfigComponent);
component = fixture.componentInstance;
htmlElement = fixture.nativeElement;
- component.traceConfig = {
+ configChangeSpy = spyOn(component.traceConfigChange, 'emit');
+
+ component.title = 'Targets';
+ component.initialTraceConfig = {
layers_trace: {
name: 'layers_trace',
- run: false,
+ enabled: true,
+ available: true,
config: {
enableConfigs: [
{
@@ -69,7 +76,22 @@
],
},
},
+ window_trace: {
+ name: 'window_trace',
+ enabled: false,
+ available: true,
+ config: undefined,
+ },
+ unavailable_trace: {
+ name: 'unavailable_trace',
+ enabled: false,
+ available: false,
+ config: undefined,
+ },
};
+ component.traceConfigStoreKey = 'TestConfigSettings';
+ component.storage = new InMemoryStorage();
+ await detectNgModelChanges();
fixture.detectChanges();
});
@@ -77,22 +99,62 @@
expect(component).toBeTruthy();
});
- it('check that trace checkbox ticked on default run', () => {
- assertDefined(component.traceConfig)['layers_trace'].run = true;
- fixture.detectChanges();
+ it('trace checkbox enabled by default', () => {
+ const config = assertDefined(component.traceConfig);
+
const box = assertDefined(htmlElement.querySelector('.trace-checkbox'));
- expect(box.innerHTML).toContain('aria-checked="true"');
- expect(box.innerHTML).toContain('layers_trace');
+ const inputElement = assertDefined(
+ box.querySelector<HTMLInputElement>('input'),
+ );
+
+ expect(box.textContent).toContain('layers_trace');
+ expect(inputElement.checked).toBeTrue();
+ expect(inputElement.ariaChecked).toEqual('true');
+ expect(config['layers_trace'].enabled).toBeTrue();
+
+ inputElement.click();
+ fixture.detectChanges();
+ expect(inputElement.checked).toBeFalse();
+ expect(inputElement.ariaChecked).toEqual('false');
+ expect(config['layers_trace'].enabled).toBeFalse();
+ expect(configChangeSpy).toHaveBeenCalledTimes(1);
});
- it('check that trace checkbox not ticked on default run', () => {
- assertDefined(component.traceConfig)['layers_trace'].run = false;
+ it('trace checkbox not enabled by default', () => {
+ const config = assertDefined(component.traceConfig);
+
+ const box = assertDefined(
+ htmlElement.querySelectorAll('.trace-checkbox').item(1),
+ );
+ const inputElement = assertDefined(
+ box.querySelector<HTMLInputElement>('input'),
+ );
+
+ expect(box.textContent).toContain('window_trace');
+ expect(inputElement.checked).toBeFalse();
+ expect(inputElement.ariaChecked).toEqual('false');
+ expect(config['window_trace'].enabled).toBeFalse();
+
+ inputElement.click();
fixture.detectChanges();
- const box = assertDefined(htmlElement.querySelector('.trace-checkbox'));
- expect(box.innerHTML).toContain('aria-checked="false"');
+ expect(inputElement.checked).toBeTrue();
+ expect(inputElement.ariaChecked).toEqual('true');
+ expect(config['window_trace'].enabled).toBeTrue();
+ expect(configChangeSpy).toHaveBeenCalledTimes(1);
});
- it('check that advanced configs show', () => {
+ it('disables checkbox for unavailable trace', () => {
+ const boxes = htmlElement.querySelectorAll('.trace-checkbox');
+
+ const unavailableBox = assertDefined(boxes.item(2));
+ const unavailableInput = assertDefined(
+ unavailableBox.querySelector('input'),
+ );
+ expect(unavailableInput.disabled).toBeTrue();
+ expect(unavailableBox.textContent).toContain('unavailable_trace');
+ });
+
+ it('enable and select configs show', () => {
const enable_config_opt = assertDefined(
htmlElement.querySelector('.enable-config-opt'),
);
@@ -106,29 +168,52 @@
expect(selection_config_opt.innerHTML).toContain('tracing level');
});
- it('check that changing enable config causes box to change', async () => {
- assertDefined(component.traceConfig)[
- 'layers_trace'
- ].config!.enableConfigs[0].enabled = false;
- fixture.detectChanges();
- await fixture.whenStable();
- expect(htmlElement.querySelector('.enable-config')?.innerHTML).toContain(
- 'aria-checked="false"',
+ it('changing enable config model value causes box to change', async () => {
+ const inputElement = assertDefined(
+ htmlElement.querySelector<HTMLInputElement>('.enable-config input'),
);
+ assertDefined(
+ assertDefined(component.traceConfig)['layers_trace'].config,
+ ).enableConfigs[0].enabled = false;
+ await detectNgModelChanges();
+ expect(inputElement.checked).toBeFalse();
+ expect(inputElement.ariaChecked).toEqual('false');
+
+ assertDefined(
+ assertDefined(component.traceConfig)['layers_trace'].config,
+ ).enableConfigs[0].enabled = true;
+ await detectNgModelChanges();
+ expect(inputElement.checked).toBeTrue();
+ expect(inputElement.ariaChecked).toEqual('true');
+ });
+
+ it('changing enable config by DOM interaction emits event', async () => {
+ const inputElement = assertDefined(
+ htmlElement.querySelector<HTMLInputElement>('.enable-config input'),
+ );
+ inputElement.click();
+ fixture.detectChanges();
+ expect(configChangeSpy).toHaveBeenCalledTimes(1);
});
it('check that changing selected config causes select to change', async () => {
- fixture.detectChanges();
- expect(htmlElement.querySelector('.config-selection')?.innerHTML).toContain(
- 'value="debug"',
+ const selectTrigger = assertDefined(
+ htmlElement.querySelector('.mat-select-trigger'),
);
- assertDefined(component.traceConfig)[
- 'layers_trace'
- ].config!.selectionConfigs[0].value = 'verbose';
+ (selectTrigger as HTMLElement).click();
fixture.detectChanges();
await fixture.whenStable();
- expect(htmlElement.querySelector('.config-selection')?.innerHTML).toContain(
- 'value="verbose"',
+ const newOption = assertDefined(
+ document.querySelector<HTMLElement>('mat-option'),
);
+ newOption.click();
+ fixture.detectChanges();
+ expect(configChangeSpy).toHaveBeenCalledTimes(1);
});
+
+ async function detectNgModelChanges() {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ fixture.detectChanges();
+ }
});
diff --git a/tools/winscope/src/common/assert_utils.ts b/tools/winscope/src/common/assert_utils.ts
index acb7924..f3ad977 100644
--- a/tools/winscope/src/common/assert_utils.ts
+++ b/tools/winscope/src/common/assert_utils.ts
@@ -14,9 +14,16 @@
* limitations under the License.
*/
-export function assertDefined<A>(value: A | null | undefined): A {
+export function assertDefined<A>(
+ value: A | null | undefined,
+ lazyErrorMessage?: () => string,
+): A {
if (value === undefined || value === null) {
- throw new Error(`Expected value, but found '${value}'`);
+ throw new Error(
+ lazyErrorMessage
+ ? lazyErrorMessage()
+ : `Expected value, but found '${value}'`,
+ );
}
return value;
}
diff --git a/tools/winscope/src/test/unit/mock_adb_connection.ts b/tools/winscope/src/test/unit/mock_adb_connection.ts
index 654a9a6..183509a 100644
--- a/tools/winscope/src/test/unit/mock_adb_connection.ts
+++ b/tools/winscope/src/test/unit/mock_adb_connection.ts
@@ -24,6 +24,8 @@
state: ConnectionState = ConnectionState.CONNECTING;
errorText = '';
files = [new File([], 'test_file')];
+ availableTracesChangeCallback: (traces: string[]) => void =
+ FunctionUtils.DO_NOTHING;
devices: AdbDevice[] = [];
private detectStateChangeInUi: () => Promise<void> =
FunctionUtils.DO_NOTHING_ASYNC;
@@ -32,9 +34,11 @@
async initialize(
detectStateChangeInUi: () => Promise<void>,
progressCallback: OnProgressUpdateType,
+ availableTracesChangeCallback: (traces: string[]) => void,
) {
this.detectStateChangeInUi = detectStateChangeInUi;
this.progressCallback = progressCallback;
+ this.availableTracesChangeCallback = availableTracesChangeCallback;
this.setState(ConnectionState.CONNECTING);
}
diff --git a/tools/winscope/src/trace_collection/adb_connection.ts b/tools/winscope/src/trace_collection/adb_connection.ts
index 323794b..551520b 100644
--- a/tools/winscope/src/trace_collection/adb_connection.ts
+++ b/tools/winscope/src/trace_collection/adb_connection.ts
@@ -19,16 +19,13 @@
import {HttpRequestStatus, HttpResponse} from 'common/http_request';
import {AdbDevice} from './adb_device';
import {ConnectionState} from './connection_state';
-import {TraceConfigurationMap} from './trace_collection_utils';
import {TraceRequest} from './trace_request';
export abstract class AdbConnection {
abstract initialize(
detectStateChangeInUi: () => Promise<void>,
progressCallback: OnProgressUpdateType,
- availableTracesChangeCallback: (
- availableTracesConfig: TraceConfigurationMap,
- ) => void,
+ availableTracesChangeCallback: (traces: string[]) => void,
): Promise<void>;
abstract restartConnection(): Promise<void>;
abstract setSecurityToken(token: string): void;
diff --git a/tools/winscope/src/trace_collection/proxy_connection.ts b/tools/winscope/src/trace_collection/proxy_connection.ts
index 053830c..275e629 100644
--- a/tools/winscope/src/trace_collection/proxy_connection.ts
+++ b/tools/winscope/src/trace_collection/proxy_connection.ts
@@ -29,7 +29,6 @@
import {AdbDevice} from './adb_device';
import {ConnectionState} from './connection_state';
import {ProxyEndpoint} from './proxy_endpoint';
-import {TraceConfigurationMap, TRACES} from './trace_collection_utils';
import {TraceRequest} from './trace_request';
export class ProxyConnection extends AdbConnection {
@@ -43,7 +42,7 @@
private errorText = '';
private securityToken = '';
private devices: AdbDevice[] = [];
- private selectedDevice: AdbDevice | undefined;
+ selectedDevice: AdbDevice | undefined;
private requestedTraces: TraceRequest[] = [];
private adbData: File[] = [];
private keepTraceAliveWorker: number | undefined;
@@ -51,16 +50,13 @@
private detectStateChangeInUi: () => Promise<void> =
FunctionUtils.DO_NOTHING_ASYNC;
private progressCallback: OnProgressUpdateType = FunctionUtils.DO_NOTHING;
- private availableTracesChangeCallback: (
- availableTracesConfig: TraceConfigurationMap,
- ) => void = FunctionUtils.DO_NOTHING;
+ private availableTracesChangeCallback: (traces: string[]) => void =
+ FunctionUtils.DO_NOTHING;
async initialize(
detectStateChangeInUi: () => Promise<void>,
progressCallback: OnProgressUpdateType,
- availableTracesChangeCallback: (
- availableTracesConfig: TraceConfigurationMap,
- ) => void,
+ availableTracesChangeCallback: (traces: string[]) => void,
): Promise<void> {
this.detectStateChangeInUi = detectStateChangeInUi;
this.progressCallback = progressCallback;
@@ -182,12 +178,10 @@
return;
case ConnectionState.IDLE:
- if (this.selectedDevice) {
+ {
const isWaylandAvailable = await this.isWaylandAvailable();
if (isWaylandAvailable) {
- const availableTracesConfig = TRACES['default'];
- Object.assign(availableTracesConfig, TRACES['arc']);
- this.availableTracesChangeCallback(availableTracesConfig);
+ this.availableTracesChangeCallback(['wayland_trace']);
}
}
return;
diff --git a/tools/winscope/src/trace_collection/proxy_connection_test.ts b/tools/winscope/src/trace_collection/proxy_connection_test.ts
index d80c08f..52d5f33 100644
--- a/tools/winscope/src/trace_collection/proxy_connection_test.ts
+++ b/tools/winscope/src/trace_collection/proxy_connection_test.ts
@@ -300,6 +300,42 @@
});
});
+ describe('wayland trace availability', () => {
+ beforeEach(() => {
+ availableTracesChangeCallback.calls.reset();
+ });
+
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ it('updates availability of wayland trace if available', async () => {
+ const successfulResponse: HttpResponse = {
+ status: HttpRequestStatus.SUCCESS,
+ type: '',
+ text: 'true',
+ body: undefined,
+ getHeader: getVersionHeader,
+ };
+ await setUpTestEnvironment(successfulResponse);
+ expect(availableTracesChangeCallback).toHaveBeenCalledOnceWith([
+ 'wayland_trace',
+ ]);
+ });
+
+ it('does not update availability of traces if call fails', async () => {
+ const unsuccessfulResponse: HttpResponse = {
+ status: HttpRequestStatus.SUCCESS,
+ type: '',
+ text: 'false',
+ body: undefined,
+ getHeader: getVersionHeader,
+ };
+ await setUpTestEnvironment(unsuccessfulResponse);
+ expect(availableTracesChangeCallback).not.toHaveBeenCalled();
+ });
+ });
+
describe('finding devices', () => {
afterEach(() => {
localStorage.clear();
@@ -322,7 +358,7 @@
});
it('fetches devices', async () => {
- const noDevicesResponse: HttpResponse = {
+ const devicesResponse: HttpResponse = {
status: HttpRequestStatus.SUCCESS,
type: 'text',
text: JSON.stringify({
@@ -331,7 +367,7 @@
body: undefined,
getHeader: getVersionHeader,
};
- await setUpTestEnvironment(noDevicesResponse);
+ await setUpTestEnvironment(devicesResponse);
checkGetDevicesRequest();
expect(connection.getState()).toEqual(ConnectionState.IDLE);
expect(connection.getDevices()).toEqual([mockDevice]);
@@ -427,7 +463,7 @@
}
function checkGetDevicesRequest(header = '') {
- expect(getSpy).toHaveBeenCalledOnceWith(
+ expect(getSpy).toHaveBeenCalledWith(
ProxyConnection.WINSCOPE_PROXY_URL + ProxyEndpoint.DEVICES,
[['Winscope-Token', header]],
undefined,
diff --git a/tools/winscope/src/trace_collection/trace_collection_utils.ts b/tools/winscope/src/trace_collection/trace_configuration.ts
similarity index 67%
rename from tools/winscope/src/trace_collection/trace_collection_utils.ts
rename to tools/winscope/src/trace_collection/trace_configuration.ts
index 4f7d94d..c9da47e 100644
--- a/tools/winscope/src/trace_collection/trace_collection_utils.ts
+++ b/tools/winscope/src/trace_collection/trace_configuration.ts
@@ -18,9 +18,10 @@
import {TraceType} from 'trace/trace_type';
export interface TraceConfiguration {
- name: string | undefined;
- run: boolean | undefined;
+ name: string;
+ enabled: boolean;
config: ConfigurationOptions | undefined;
+ available: boolean;
}
export interface TraceConfigurationMap {
@@ -112,84 +113,116 @@
},
];
-const traceConfigurations: TraceConfigurationMap = {
+const traceDefaultConfig: TraceConfigurationMap = {
layers_trace: {
name: TRACE_INFO[TraceType.SURFACE_FLINGER].name,
- run: true,
+ enabled: true,
config: {
enableConfigs: sfTraceEnableConfigs,
selectionConfigs: sfTraceSelectionConfigs,
},
+ available: true,
},
window_trace: {
name: TRACE_INFO[TraceType.WINDOW_MANAGER].name,
- run: true,
+ enabled: true,
config: {
enableConfigs: [],
selectionConfigs: wmTraceSelectionConfigs,
},
+ available: true,
},
screen_recording: {
name: TRACE_INFO[TraceType.SCREEN_RECORDING].name,
- run: true,
+ enabled: true,
config: undefined,
+ available: true,
},
ime: {
name: 'IME',
- run: true,
+ enabled: true,
config: undefined,
+ available: true,
},
transactions: {
name: TRACE_INFO[TraceType.TRANSACTIONS].name,
- run: true,
+ enabled: true,
config: undefined,
+ available: true,
},
proto_log: {
name: TRACE_INFO[TraceType.PROTO_LOG].name,
- run: false,
+ enabled: false,
config: undefined,
+ available: true,
},
wayland_trace: {
name: TRACE_INFO[TraceType.WAYLAND].name,
- run: false,
+ enabled: false,
config: undefined,
+ available: false,
},
eventlog: {
name: TRACE_INFO[TraceType.EVENT_LOG].name,
- run: false,
+ enabled: false,
config: undefined,
+ available: true,
},
transition_traces: {
name: TRACE_INFO[TraceType.SHELL_TRANSITION].name,
- run: false,
+ enabled: false,
config: undefined,
+ available: true,
},
view_capture_traces: {
name: 'View Capture',
- run: false,
+ enabled: false,
config: undefined,
+ available: true,
},
input: {
name: 'Input',
- run: false,
+ enabled: false,
config: undefined,
+ available: true,
},
};
-export const TRACES: {[key: string]: TraceConfigurationMap} = {
- default: {
- window_trace: traceConfigurations['window_trace'],
- layers_trace: traceConfigurations['layers_trace'],
- transactions: traceConfigurations['transactions'],
- proto_log: traceConfigurations['proto_log'],
- screen_recording: traceConfigurations['screen_recording'],
- ime: traceConfigurations['ime'],
- eventlog: traceConfigurations['eventlog'],
- transition_traces: traceConfigurations['transition_traces'],
- view_capture_trace: traceConfigurations['view_capture_traces'],
- input: traceConfigurations['input'],
- },
- arc: {
- wayland_trace: traceConfigurations['wayland_trace'],
- },
-};
+export function makeDefaultTraceConfigMap(): TraceConfigurationMap {
+ return structuredClone({
+ window_trace: traceDefaultConfig['window_trace'],
+ layers_trace: traceDefaultConfig['layers_trace'],
+ transactions: traceDefaultConfig['transactions'],
+ proto_log: traceDefaultConfig['proto_log'],
+ screen_recording: traceDefaultConfig['screen_recording'],
+ ime: traceDefaultConfig['ime'],
+ eventlog: traceDefaultConfig['eventlog'],
+ transition_traces: traceDefaultConfig['transition_traces'],
+ view_capture_trace: traceDefaultConfig['view_capture_traces'],
+ input: traceDefaultConfig['input'],
+ wayland_trace: traceDefaultConfig['wayland_trace'],
+ });
+}
+
+export function makeDefaultDumpConfigMap(): TraceConfigurationMap {
+ return structuredClone({
+ window_dump: {
+ name: 'Window Manager',
+ enabled: true,
+ config: undefined,
+ available: true,
+ },
+ layers_dump: {
+ name: 'Surface Flinger',
+ enabled: true,
+ config: undefined,
+ available: true,
+ },
+ screenshot: {
+ name: 'Screenshot',
+ enabled: true,
+ config: undefined,
+ available: true,
+ },
+ });
+}