Add jank CUJ tag viewer
Screenshot: https://screenshot.googleplex.com/6PLjrtN7UZq3gdo
Test: npm run test:presubmit
Change-Id: Ibe149f2a573e068a1b3a04a50d99d91d42e74da1
diff --git a/tools/winscope/src/app/app_module.ts b/tools/winscope/src/app/app_module.ts
index cf7c9c1..6fada3c 100644
--- a/tools/winscope/src/app/app_module.ts
+++ b/tools/winscope/src/app/app_module.ts
@@ -63,6 +63,7 @@
import {UserOptionsComponent} from 'viewers/components/user_options_component';
import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component';
import {ViewCapturePropertyGroupsComponent} from 'viewers/components/view_capture_property_groups_component';
+import {ViewerJankCujsComponent} from 'viewers/viewer_jank_cujs/viewer_jank_cujs_component';
import {ProtologScrollDirective} from 'viewers/viewer_protolog/scroll_strategy/protolog_scroll_directive';
import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component';
import {ViewerScreenRecordingComponent} from 'viewers/viewer_screen_recording/viewer_screen_recording_component';
@@ -102,6 +103,7 @@
ViewerSurfaceFlingerComponent,
ViewerInputMethodComponent,
ViewerProtologComponent,
+ ViewerJankCujsComponent,
ViewerTransactionsComponent,
ViewerScreenRecordingComponent,
ViewerTransitionsComponent,
diff --git a/tools/winscope/src/app/components/app_component.ts b/tools/winscope/src/app/components/app_component.ts
index b5fced6..f502a9f 100644
--- a/tools/winscope/src/app/components/app_component.ts
+++ b/tools/winscope/src/app/components/app_component.ts
@@ -60,6 +60,7 @@
import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles';
import {ViewerInputMethodComponent} from 'viewers/components/viewer_input_method_component';
import {Viewer} from 'viewers/viewer';
+import {ViewerJankCujsComponent} from 'viewers/viewer_jank_cujs/viewer_jank_cujs_component';
import {ViewerProtologComponent} from 'viewers/viewer_protolog/viewer_protolog_component';
import {ViewerScreenRecordingComponent} from 'viewers/viewer_screen_recording/viewer_screen_recording_component';
import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component';
@@ -440,6 +441,12 @@
createCustomElement(ViewerViewCaptureComponent, {injector}),
);
}
+ if (!customElements.get('viewer-jank-cujs')) {
+ customElements.define(
+ 'viewer-jank-cujs',
+ createCustomElement(ViewerJankCujsComponent, {injector}),
+ );
+ }
this.traceConfigStorage =
globalConfig.MODE === 'PROD' ? localStorage : new InMemoryStorage();
diff --git a/tools/winscope/src/parsers/events/traces_parser_cujs.ts b/tools/winscope/src/parsers/events/traces_parser_cujs.ts
index 13861aa..f377671 100644
--- a/tools/winscope/src/parsers/events/traces_parser_cujs.ts
+++ b/tools/winscope/src/parsers/events/traces_parser_cujs.ts
@@ -15,6 +15,7 @@
*/
import {assertDefined} from 'common/assert_utils';
+import {Timestamp} from 'common/time';
import {ParserTimestampConverter} from 'common/timestamp_converter';
import {AbstractTracesParser} from 'parsers/traces/abstract_traces_parser';
import {CoarseVersion} from 'trace/coarse_version';
@@ -26,7 +27,6 @@
import {CujType} from './cuj_type';
import {EventTag} from './event_tag';
import {AddCujProperties} from './operations/add_cuj_properties';
-import { Timestamp } from 'common/time';
export class TracesParserCujs extends AbstractTracesParser<PropertyTreeNode> {
private static readonly AddCujProperties = new AddCujProperties();
@@ -75,9 +75,7 @@
this.timestamps = [];
for (let index = 0; index < this.getLengthEntries(); index++) {
const entry = await this.getEntry(index);
- const timestamp = entry
- ?.getChildByName('startTimestamp')
- ?.getValue();
+ const timestamp = entry?.getChildByName('startTimestamp')?.getValue();
this.timestamps.push(timestamp);
}
}
diff --git a/tools/winscope/src/trace/trace_type.ts b/tools/winscope/src/trace/trace_type.ts
index 8bbdb3b..f5ed102 100644
--- a/tools/winscope/src/trace/trace_type.ts
+++ b/tools/winscope/src/trace/trace_type.ts
@@ -99,6 +99,7 @@
TraceType.PROTO_LOG,
TraceType.VIEW_CAPTURE,
TraceType.TRANSITION,
+ TraceType.CUJS,
];
static isTraceTypeWithViewer(t: TraceType): boolean {
diff --git a/tools/winscope/src/viewers/common/ui_data_log.ts b/tools/winscope/src/viewers/common/ui_data_log.ts
index 134d48f..461509a 100644
--- a/tools/winscope/src/viewers/common/ui_data_log.ts
+++ b/tools/winscope/src/viewers/common/ui_data_log.ts
@@ -70,6 +70,9 @@
DISPATCH_TIME = 'Dispatch Time',
DURATION = 'Duration',
STATUS = 'Status',
+ CUJ_TYPE = 'Type',
+ START_TIME = 'Start Time',
+ END_TIME = 'End Time',
}
export const LogFieldClassNames: ReadonlyMap<LogFieldName, string> = new Map([
@@ -86,8 +89,11 @@
[LogFieldName.TEXT, 'text'],
[LogFieldName.TRANSITION_ID, 'transition-id'],
[LogFieldName.TRANSITION_TYPE, 'transition-type'],
+ [LogFieldName.CUJ_TYPE, 'jank_cuj-type'],
[LogFieldName.SEND_TIME, 'send-time time'],
[LogFieldName.DISPATCH_TIME, 'dispatch-time time'],
+ [LogFieldName.START_TIME, 'start-time time'],
+ [LogFieldName.END_TIME, 'end-time time'],
[LogFieldName.DURATION, 'duration'],
[LogFieldName.STATUS, 'status'],
]);
diff --git a/tools/winscope/src/viewers/components/styles/log_component.styles.ts b/tools/winscope/src/viewers/components/styles/log_component.styles.ts
index fc2818f..dbf8b68 100644
--- a/tools/winscope/src/viewers/components/styles/log_component.styles.ts
+++ b/tools/winscope/src/viewers/components/styles/log_component.styles.ts
@@ -130,11 +130,11 @@
flex: 2;
}
- .dispatch-time {
- flex: 4;
+ .jank_cuj-type {
+ flex: 5;
}
- .send-time {
+ .start-time, .end-time, .dispatch-time, .send-time {
flex: 4;
}
diff --git a/tools/winscope/src/viewers/viewer_factory.ts b/tools/winscope/src/viewers/viewer_factory.ts
index 61b9a65..d255a45 100644
--- a/tools/winscope/src/viewers/viewer_factory.ts
+++ b/tools/winscope/src/viewers/viewer_factory.ts
@@ -22,6 +22,7 @@
import {ViewerInputMethodClients} from './viewer_input_method_clients/viewer_input_method_clients';
import {ViewerInputMethodManagerService} from './viewer_input_method_manager_service/viewer_input_method_manager_service';
import {ViewerInputMethodService} from './viewer_input_method_service/viewer_input_method_service';
+import {ViewerJankCujs} from './viewer_jank_cujs/viewer_jank_cujs';
import {ViewerProtoLog} from './viewer_protolog/viewer_protolog';
import {ViewerScreenshot} from './viewer_screen_recording/viewer_screenshot';
import {ViewerScreenRecording} from './viewer_screen_recording/viewer_screen_recording';
@@ -43,6 +44,7 @@
ViewerScreenRecording,
ViewerScreenshot,
ViewerTransitions,
+ ViewerJankCujs,
];
createViewers(traces: Traces, storage: Storage): Viewer[] {
diff --git a/tools/winscope/src/viewers/viewer_jank_cujs/presenter.ts b/tools/winscope/src/viewers/viewer_jank_cujs/presenter.ts
new file mode 100644
index 0000000..09ab5df
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_jank_cujs/presenter.ts
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {assertDefined} from 'common/assert_utils';
+import {TimeDuration} from 'common/time_duration';
+import {Trace} from 'trace/trace';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {
+ AbstractLogViewerPresenter,
+ NotifyLogViewCallbackType,
+} from 'viewers/common/abstract_log_viewer_presenter';
+import {LogPresenter} from 'viewers/common/log_presenter';
+import {PropertiesPresenter} from 'viewers/common/properties_presenter';
+import {LogField, LogFieldName} from 'viewers/common/ui_data_log';
+import {CujEntry, CujStatus, CujType, UiData} from './ui_data';
+
+export class Presenter extends AbstractLogViewerPresenter {
+ static readonly FIELD_NAMES = [
+ LogFieldName.CUJ_TYPE,
+ LogFieldName.START_TIME,
+ LogFieldName.END_TIME,
+ LogFieldName.DURATION,
+ LogFieldName.STATUS,
+ ];
+ private static readonly VALUE_NA = 'N/A';
+
+ private isInitialized = false;
+ private transitionTrace: Trace<PropertyTreeNode>;
+
+ protected override logPresenter = new LogPresenter(false);
+ protected override propertiesPresenter = new PropertiesPresenter({}, [], []);
+
+ constructor(
+ trace: Trace<PropertyTreeNode>,
+ notifyViewCallback: NotifyLogViewCallbackType,
+ ) {
+ super(trace, notifyViewCallback, UiData.EMPTY);
+ this.transitionTrace = trace;
+ }
+
+ protected async initializeIfNeeded() {
+ if (this.isInitialized) {
+ return;
+ }
+
+ const allEntries = await this.makeUiDataEntries();
+
+ this.logPresenter.setAllEntries(allEntries);
+ this.logPresenter.setHeaders(Presenter.FIELD_NAMES);
+ this.refreshUIData(UiData.EMPTY);
+ this.isInitialized = true;
+ }
+
+ private async makeUiDataEntries(): Promise<CujEntry[]> {
+ const cujs: CujEntry[] = [];
+ for (
+ let traceIndex = 0;
+ traceIndex < this.transitionTrace.lengthEntries;
+ ++traceIndex
+ ) {
+ const entry = assertDefined(this.trace.getEntry(traceIndex));
+ const cujNode = await entry.getValue();
+
+ let status: CujStatus | undefined;
+ let statusIcon: string | undefined;
+ let statusIconColor: string | undefined;
+ if (assertDefined(cujNode.getChildByName('canceled')).getValue()) {
+ status = CujStatus.CANCELLED;
+ statusIcon = 'close';
+ statusIconColor = 'red';
+ } else {
+ status = CujStatus.EXECUTED;
+ statusIcon = 'check';
+ statusIconColor = 'green';
+ }
+
+ const startTs = cujNode.getChildByName('startTimestamp')?.getValue();
+ const endTs = cujNode.getChildByName('endTimestamp')?.getValue();
+
+ let timeDiff: TimeDuration | undefined = undefined;
+ if (startTs && endTs) {
+ const timeDiffNs = endTs.minus(startTs.getValueNs()).getValueNs();
+ timeDiff = new TimeDuration(timeDiffNs);
+ }
+
+ const cujTypeId = assertDefined(
+ cujNode.getChildByName('cujType'),
+ ).getValue();
+
+ const fields: LogField[] = [
+ {
+ name: LogFieldName.CUJ_TYPE,
+ value: `${CujType[cujTypeId]} (${cujTypeId})`,
+ },
+ {
+ name: LogFieldName.START_TIME,
+ value: startTs ?? Presenter.VALUE_NA,
+ },
+ {
+ name: LogFieldName.END_TIME,
+ value: endTs ?? Presenter.VALUE_NA,
+ },
+ {
+ name: LogFieldName.DURATION,
+ value: timeDiff?.format() ?? Presenter.VALUE_NA,
+ },
+ {
+ name: LogFieldName.STATUS,
+ value: status ?? Presenter.VALUE_NA,
+ icon: statusIcon,
+ iconColor: statusIconColor,
+ },
+ ];
+ cujs.push(new CujEntry(entry, fields, cujNode));
+ }
+
+ return cujs;
+ }
+}
diff --git a/tools/winscope/src/viewers/viewer_jank_cujs/presenter_test.ts b/tools/winscope/src/viewers/viewer_jank_cujs/presenter_test.ts
new file mode 100644
index 0000000..9941762
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_jank_cujs/presenter_test.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANYf KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {assertDefined} from 'common/assert_utils';
+import {TracePositionUpdate} from 'messaging/winscope_event';
+import {TracesBuilder} from 'test/unit/traces_builder';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Parser} from 'trace/parser';
+import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
+import {TraceType} from 'trace/trace_type';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {NotifyLogViewCallbackType} from 'viewers/common/abstract_log_viewer_presenter';
+import {AbstractLogViewerPresenterTest} from 'viewers/common/abstract_log_viewer_presenter_test';
+import {Presenter} from './presenter';
+
+class PresenterJankCujsTest extends AbstractLogViewerPresenterTest {
+ private trace: Trace<PropertyTreeNode> | undefined;
+ private positionUpdate: TracePositionUpdate | undefined;
+ private secondPositionUpdate: TracePositionUpdate | undefined;
+
+ override readonly shouldExecuteHeaderTests = true;
+ override readonly shouldExecuteFilterTests = false;
+ override readonly shouldExecuteCurrentIndexTests = false;
+ override readonly shouldExecutePropertiesTests = true;
+
+ override readonly totalOutputEntries = 16;
+ override readonly expectedIndexOfSecondPositionUpdate = 2;
+ override readonly logEntryClickIndex = 3;
+
+ override async setUpTestEnvironment(): Promise<void> {
+ const parser = (await UnitTestUtils.getTracesParser([
+ 'traces/eventlog.winscope',
+ ])) as Parser<PropertyTreeNode>;
+
+ this.trace = new TraceBuilder<PropertyTreeNode>()
+ .setType(TraceType.CUJS)
+ .setParser(parser)
+ .build();
+
+ this.positionUpdate = TracePositionUpdate.fromTraceEntry(
+ this.trace.getEntry(0),
+ );
+ this.secondPositionUpdate = TracePositionUpdate.fromTraceEntry(
+ this.trace.getEntry(2),
+ );
+ }
+
+ override createPresenterWithEmptyTrace(
+ callback: NotifyLogViewCallbackType,
+ ): Presenter {
+ const traces = new TracesBuilder()
+ .setEntries(TraceType.TRANSITION, [])
+ .build();
+ const trace = assertDefined(traces.getTrace(TraceType.TRANSITION));
+ return new Presenter(trace, callback);
+ }
+
+ override async createPresenter(
+ callback: NotifyLogViewCallbackType,
+ ): Promise<Presenter> {
+ const trace = assertDefined(this.trace);
+ const traces = new Traces();
+ traces.addTrace(trace);
+
+ const presenter = new Presenter(
+ trace,
+ callback as NotifyLogViewCallbackType,
+ );
+ await presenter.onAppEvent(this.getPositionUpdate()); // trigger initialization
+ return presenter;
+ }
+
+ override getPositionUpdate(): TracePositionUpdate {
+ return assertDefined(this.positionUpdate);
+ }
+
+ override getSecondPositionUpdate(): TracePositionUpdate {
+ return assertDefined(this.secondPositionUpdate);
+ }
+}
+
+describe('PresenterJankCujsTest', () => {
+ new PresenterJankCujsTest().execute();
+});
diff --git a/tools/winscope/src/viewers/viewer_jank_cujs/ui_data.ts b/tools/winscope/src/viewers/viewer_jank_cujs/ui_data.ts
new file mode 100644
index 0000000..ea4aa95
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_jank_cujs/ui_data.ts
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TraceEntry} from 'trace/trace';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {
+ LogEntry,
+ LogField,
+ LogFieldName,
+ UiDataLog,
+} from 'viewers/common/ui_data_log';
+import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
+
+export class UiData implements UiDataLog {
+ constructor(
+ public headers: LogFieldName[],
+ public entries: LogEntry[],
+ public selectedIndex: undefined | number,
+ public scrollToIndex: undefined | number,
+ public propertiesTree: undefined | UiPropertyTreeNode,
+ ) {}
+
+ static EMPTY = new UiData([], [], undefined, undefined, undefined);
+}
+export class CujEntry implements LogEntry {
+ constructor(
+ public traceEntry: TraceEntry<PropertyTreeNode>,
+ public fields: LogField[],
+ public propertiesTree: PropertyTreeNode | undefined,
+ ) {}
+}
+
+export enum CujStatus {
+ EXECUTED = 'EXECUTED',
+ CANCELLED = 'CANCELLED',
+}
+
+/**
+ * Source of truth found at:`frameworks/base/core/java/com/android/internal/jank/Cuj.java`
+ */
+export enum CujType {
+ NOTIFICATION_SHADE_EXPAND_COLLAPSE = 0,
+ NOTIFICATION_SHADE_SCROLL_FLING = 2,
+ NOTIFICATION_SHADE_ROW_EXPAND = 3,
+ NOTIFICATION_SHADE_ROW_SWIPE = 4,
+ NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE = 5,
+ NOTIFICATION_SHADE_QS_SCROLL_SWIPE = 6,
+ LAUNCHER_APP_LAUNCH_FROM_RECENTS = 7,
+ LAUNCHER_APP_LAUNCH_FROM_ICON = 8,
+ LAUNCHER_APP_CLOSE_TO_HOME = 9,
+ LAUNCHER_APP_CLOSE_TO_PIP = 10,
+ LAUNCHER_QUICK_SWITCH = 11,
+ NOTIFICATION_HEADS_UP_APPEAR = 12,
+ NOTIFICATION_HEADS_UP_DISAPPEAR = 13,
+ NOTIFICATION_ADD = 14,
+ NOTIFICATION_REMOVE = 15,
+ NOTIFICATION_APP_START = 16,
+ LOCKSCREEN_PASSWORD_APPEAR = 17,
+ LOCKSCREEN_PATTERN_APPEAR = 18,
+ LOCKSCREEN_PIN_APPEAR = 19,
+ LOCKSCREEN_PASSWORD_DISAPPEAR = 20,
+ LOCKSCREEN_PATTERN_DISAPPEAR = 21,
+ LOCKSCREEN_PIN_DISAPPEAR = 22,
+ LOCKSCREEN_TRANSITION_FROM_AOD = 23,
+ LOCKSCREEN_TRANSITION_TO_AOD = 24,
+ LAUNCHER_OPEN_ALL_APPS = 25,
+ LAUNCHER_ALL_APPS_SCROLL = 26,
+ LAUNCHER_APP_LAUNCH_FROM_WIDGET = 27,
+ SETTINGS_PAGE_SCROLL = 28,
+ LOCKSCREEN_UNLOCK_ANIMATION = 29,
+ SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON = 30,
+ SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER = 31,
+ SHADE_APP_LAUNCH_FROM_QS_TILE = 32,
+ SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON = 33,
+ STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP = 34,
+ PIP_TRANSITION = 35,
+ WALLPAPER_TRANSITION = 36,
+ USER_SWITCH = 37,
+ SPLASHSCREEN_AVD = 38,
+ SPLASHSCREEN_EXIT_ANIM = 39,
+ SCREEN_OFF = 40,
+ SCREEN_OFF_SHOW_AOD = 41,
+ ONE_HANDED_ENTER_TRANSITION = 42,
+ ONE_HANDED_EXIT_TRANSITION = 43,
+ UNFOLD_ANIM = 44,
+ SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS = 45,
+ SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS = 46,
+ SUW_LOADING_TO_NEXT_FLOW = 47,
+ SUW_LOADING_SCREEN_FOR_STATUS = 48,
+ SPLIT_SCREEN_ENTER = 49,
+ SPLIT_SCREEN_EXIT = 50,
+ LOCKSCREEN_LAUNCH_CAMERA = 51, // reserved.
+ SPLIT_SCREEN_RESIZE = 52,
+ SETTINGS_SLIDER = 53,
+ TAKE_SCREENSHOT = 54,
+ VOLUME_CONTROL = 55,
+ BIOMETRIC_PROMPT_TRANSITION = 56,
+ SETTINGS_TOGGLE = 57,
+ SHADE_DIALOG_OPEN = 58,
+ USER_DIALOG_OPEN = 59,
+ TASKBAR_EXPAND = 60,
+ TASKBAR_COLLAPSE = 61,
+ SHADE_CLEAR_ALL = 62,
+ LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63,
+ LOCKSCREEN_OCCLUSION = 64,
+ RECENTS_SCROLLING = 65,
+ LAUNCHER_APP_SWIPE_TO_RECENTS = 66,
+ LAUNCHER_CLOSE_ALL_APPS_SWIPE = 67,
+ LAUNCHER_CLOSE_ALL_APPS_TO_HOME = 68,
+ LOCKSCREEN_CLOCK_MOVE_ANIMATION = 70,
+ LAUNCHER_OPEN_SEARCH_RESULT = 71,
+ // 72 - 77 are reserved for b/281564325.
+
+ /**
+ * In some cases when we do not have any end-target, we play a simple slide-down animation.
+ * eg: Open an app from Overview/Task switcher such that there is no home-screen icon.
+ * eg: Exit the app using back gesture.
+ */
+ LAUNCHER_APP_CLOSE_TO_HOME_FALLBACK = 78,
+ // 79 is reserved.
+ IME_INSETS_SHOW_ANIMATION = 80,
+ IME_INSETS_HIDE_ANIMATION = 81,
+
+ SPLIT_SCREEN_DOUBLE_TAP_DIVIDER = 82,
+
+ LAUNCHER_UNFOLD_ANIM = 83,
+
+ PREDICTIVE_BACK_CROSS_ACTIVITY = 84,
+ PREDICTIVE_BACK_CROSS_TASK = 85,
+ PREDICTIVE_BACK_HOME = 86,
+ // 87 is reserved - previously assigned to deprecated CUJ_LAUNCHER_SEARCH_QSB_OPEN.
+ BACK_PANEL_ARROW = 88,
+ LAUNCHER_CLOSE_ALL_APPS_BACK = 89,
+ LAUNCHER_SEARCH_QSB_WEB_SEARCH = 90,
+ LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = 91,
+ LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = 92,
+ LAUNCHER_SAVE_APP_PAIR = 93,
+ LAUNCHER_ALL_APPS_SEARCH_BACK = 95,
+ LAUNCHER_TASKBAR_ALL_APPS_CLOSE_BACK = 96,
+ LAUNCHER_TASKBAR_ALL_APPS_SEARCH_BACK = 97,
+ LAUNCHER_WIDGET_PICKER_CLOSE_BACK = 98,
+ LAUNCHER_WIDGET_PICKER_SEARCH_BACK = 99,
+ LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK = 100,
+ LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK = 101,
+ LAUNCHER_PRIVATE_SPACE_LOCK = 102,
+ LAUNCHER_PRIVATE_SPACE_UNLOCK = 103,
+}
diff --git a/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs.ts b/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs.ts
new file mode 100644
index 0000000..28f21e8
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent} from 'messaging/winscope_event_emitter';
+import {Trace} from 'trace/trace';
+import {Traces} from 'trace/traces';
+import {TraceType} from 'trace/trace_type';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {NotifyLogViewCallbackType} from 'viewers/common/abstract_log_viewer_presenter';
+import {View, Viewer, ViewType} from 'viewers/viewer';
+import {Presenter} from './presenter';
+import {UiData} from './ui_data';
+
+export class ViewerJankCujs implements Viewer {
+ static readonly DEPENDENCIES: TraceType[] = [TraceType.CUJS];
+
+ private readonly trace: Trace<PropertyTreeNode>;
+ private readonly htmlElement: HTMLElement;
+ private readonly presenter: Presenter;
+ private readonly view: View;
+
+ constructor(trace: Trace<PropertyTreeNode>, traces: Traces) {
+ this.trace = trace;
+ this.htmlElement = document.createElement('viewer-jank-cujs');
+ const notifyViewCallback = (data: UiData) => {
+ (this.htmlElement as any).inputData = data;
+ };
+ this.presenter = new Presenter(
+ trace,
+ notifyViewCallback as NotifyLogViewCallbackType,
+ );
+ this.presenter.addEventListeners(this.htmlElement);
+
+ this.view = new View(
+ ViewType.TAB,
+ this.getTraces(),
+ this.htmlElement,
+ 'Jank CUJs',
+ );
+ }
+
+ async onWinscopeEvent(event: WinscopeEvent) {
+ await this.presenter.onAppEvent(event);
+ }
+
+ setEmitEvent(callback: EmitEvent) {
+ this.presenter.setEmitEvent(callback);
+ }
+
+ getViews(): View[] {
+ return [this.view];
+ }
+
+ getTraces(): Array<Trace<PropertyTreeNode>> {
+ return [this.trace];
+ }
+}
diff --git a/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs_component.ts b/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs_component.ts
new file mode 100644
index 0000000..bc4384c
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs_component.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, Input, ViewChild} from '@angular/core';
+import {TraceType} from 'trace/trace_type';
+import {LogComponent} from 'viewers/common/log_component';
+import {viewerCardStyle} from 'viewers/components/styles/viewer_card.styles';
+import {UiData} from './ui_data';
+
+@Component({
+ selector: 'viewer-jank-cujs',
+ template: `
+ <div class="card-grid">
+ <log-view
+ class="log-view"
+ [selectedIndex]="inputData?.selectedIndex"
+ [scrollToIndex]="inputData?.scrollToIndex"
+ [currentIndex]="inputData?.currentIndex"
+ [entries]="inputData?.entries"
+ [headers]="inputData?.headers"
+ [traceType]="${TraceType.CUJS}"
+ [showTraceEntryTimes]="false"
+ [showCurrentTimeButton]="false">
+ </log-view>
+ </div>
+ `,
+ styles: [viewerCardStyle],
+})
+export class ViewerJankCujsComponent {
+ @Input() inputData: UiData | undefined;
+
+ @ViewChild(LogComponent)
+ logComponent?: LogComponent;
+}
diff --git a/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs_component_test.ts b/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs_component_test.ts
new file mode 100644
index 0000000..7a54ccb
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_jank_cujs/viewer_jank_cujs_component_test.ts
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ScrollingModule} from '@angular/cdk/scrolling';
+import {
+ ComponentFixture,
+ ComponentFixtureAutoDetect,
+ TestBed,
+} from '@angular/core/testing';
+import {MatDividerModule} from '@angular/material/divider';
+import {MatIconModule} from '@angular/material/icon';
+import {assertDefined} from 'common/assert_utils';
+import {TimeDuration} from 'common/time_duration';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {TraceBuilder} from 'test/unit/trace_builder';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Parser} from 'trace/parser';
+import {Trace, TraceEntry} from 'trace/trace';
+import {TraceType} from 'trace/trace_type';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {LogComponent} from 'viewers/common/log_component';
+import {LogEntry, LogField, LogFieldName} from 'viewers/common/ui_data_log';
+import {CollapsedSectionsComponent} from 'viewers/components/collapsed_sections_component';
+import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component';
+import {PropertiesComponent} from 'viewers/components/properties_component';
+import {PropertyTreeNodeDataViewComponent} from 'viewers/components/property_tree_node_data_view_component';
+import {TreeComponent} from 'viewers/components/tree_component';
+import {TreeNodeComponent} from 'viewers/components/tree_node_component';
+import {Presenter} from './presenter';
+import {CujStatus, CujType, UiData} from './ui_data';
+import {ViewerJankCujsComponent} from './viewer_jank_cujs_component';
+
+describe('ViewerJankCujsComponent', () => {
+ let fixture: ComponentFixture<ViewerJankCujsComponent>;
+ let component: ViewerJankCujsComponent;
+ let htmlElement: HTMLElement;
+
+ let trace: Trace<PropertyTreeNode>;
+ let entry: TraceEntry<PropertyTreeNode>;
+
+ beforeAll(async () => {
+ const parser = (await UnitTestUtils.getTracesParser([
+ 'traces/eventlog.winscope',
+ ])) as Parser<PropertyTreeNode>;
+
+ trace = new TraceBuilder<PropertyTreeNode>()
+ .setParser(parser)
+ .setType(TraceType.CUJS)
+ .build();
+
+ entry = trace.getEntry(0);
+ });
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
+ imports: [MatDividerModule, ScrollingModule, MatIconModule],
+ declarations: [
+ ViewerJankCujsComponent,
+ TreeComponent,
+ TreeNodeComponent,
+ PropertyTreeNodeDataViewComponent,
+ PropertiesComponent,
+ CollapsedSectionsComponent,
+ CollapsibleSectionTitleComponent,
+ LogComponent,
+ ],
+ schemas: [],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ViewerJankCujsComponent);
+ component = fixture.componentInstance;
+ htmlElement = fixture.nativeElement;
+
+ component.inputData = makeUiData();
+ fixture.detectChanges();
+ });
+
+ it('can be created', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('renders entries', () => {
+ expect(htmlElement.querySelector('.scroll')).toBeTruthy();
+
+ const entry = assertDefined(htmlElement.querySelector('.scroll .entry'));
+ expect(entry.innerHTML).toContain('LOCKSCREEN_PASSWORD_DISAPPEAR');
+ expect(entry.innerHTML).toContain('30ns');
+ });
+
+ function makeUiData(): UiData {
+ let mockTransitionIdCounter = 0;
+
+ const cujEntries = [
+ createMockCujEntry(entry, 20, 30, mockTransitionIdCounter++),
+ createMockCujEntry(entry, 66, 42, 50, CujStatus.CANCELLED),
+ createMockCujEntry(entry, 46, 49, mockTransitionIdCounter++),
+ createMockCujEntry(entry, 59, 58, 70, CujStatus.EXECUTED),
+ ];
+
+ const uiData = UiData.EMPTY;
+ uiData.entries = cujEntries;
+ uiData.selectedIndex = 0;
+ uiData.headers = Presenter.FIELD_NAMES;
+ return uiData;
+ }
+
+ function createMockCujEntry(
+ entry: TraceEntry<PropertyTreeNode>,
+ cujTypeId: number,
+ startTsNanos: number,
+ endTsNanos: number,
+ status = CujStatus.EXECUTED,
+ ): LogEntry {
+ const fields: LogField[] = [
+ {
+ name: LogFieldName.CUJ_TYPE,
+ value: `${CujType[cujTypeId]} (${cujTypeId})`,
+ },
+ {
+ name: LogFieldName.START_TIME,
+ value: TimestampConverterUtils.makeElapsedTimestamp(
+ BigInt(startTsNanos),
+ ),
+ },
+ {
+ name: LogFieldName.END_TIME,
+ value: TimestampConverterUtils.makeElapsedTimestamp(BigInt(endTsNanos)),
+ },
+ {
+ name: LogFieldName.DURATION,
+ value: new TimeDuration(BigInt(endTsNanos - startTsNanos)).format(),
+ },
+ {
+ name: LogFieldName.STATUS,
+ value: status,
+ icon: 'check',
+ iconColor: 'green',
+ },
+ ];
+
+ return {
+ traceEntry: entry,
+ fields,
+ propertiesTree: undefined,
+ };
+ }
+});