xref: /aosp_15_r20/development/tools/winscope/src/viewers/viewer_transitions/presenter.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2023 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 {Store} from 'common/store';
19import {CustomQueryType} from 'trace/custom_query';
20import {Trace} from 'trace/trace';
21import {Traces} from 'trace/traces';
22import {TraceType} from 'trace/trace_type';
23import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
24import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
25import {
26  AbstractLogViewerPresenter,
27  NotifyLogViewCallbackType,
28} from 'viewers/common/abstract_log_viewer_presenter';
29import {LogSelectFilter} from 'viewers/common/log_filters';
30import {LogPresenter} from 'viewers/common/log_presenter';
31import {PropertiesPresenter} from 'viewers/common/properties_presenter';
32import {TextFilter} from 'viewers/common/text_filter';
33import {ColumnSpec, LogField, LogHeader} from 'viewers/common/ui_data_log';
34import {UpdateTransitionChangesNames} from './operations/update_transition_changes_names';
35import {TransitionsEntry, TransitionStatus, UiData} from './ui_data';
36
37export class Presenter extends AbstractLogViewerPresenter<
38  UiData,
39  PropertyTreeNode
40> {
41  private static readonly COLUMNS = {
42    id: {name: 'Id', cssClass: 'transition-id right-align'},
43    type: {name: 'Type', cssClass: 'transition-type'},
44    sendTime: {name: 'Send Time', cssClass: 'send-time time'},
45    dispatchTime: {name: 'Dispatch Time', cssClass: 'dispatch-time time'},
46    duration: {name: 'Duration', cssClass: 'duration right-align'},
47    handler: {name: 'Handler', cssClass: 'handler'},
48    participants: {name: 'Participants', cssClass: 'participants'},
49    flags: {name: 'Flags', cssClass: 'flags'},
50    status: {name: 'Status', cssClass: 'status right-align'},
51  };
52  private transitionTrace: Trace<PropertyTreeNode>;
53  private surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined;
54  private windowManagerTrace: Trace<HierarchyTreeNode> | undefined;
55  private layerIdToName = new Map<number, string>();
56  private windowTokenToTitle = new Map<string, string>();
57  private updateTransitionChangesNames = new UpdateTransitionChangesNames(
58    this.layerIdToName,
59    this.windowTokenToTitle,
60  );
61  private uniqueFieldValues = new Map<ColumnSpec, Set<string>>();
62
63  protected override keepCalculated = false;
64  protected override logPresenter = new LogPresenter<TransitionsEntry>(false);
65  protected override propertiesPresenter = new PropertiesPresenter(
66    {},
67    new TextFilter(),
68    [],
69  );
70
71  constructor(
72    trace: Trace<PropertyTreeNode>,
73    traces: Traces,
74    readonly storage: Store,
75    notifyViewCallback: NotifyLogViewCallbackType<UiData>,
76  ) {
77    super(trace, notifyViewCallback, UiData.createEmpty());
78    this.transitionTrace = trace;
79    this.surfaceFlingerTrace = traces.getTrace(TraceType.SURFACE_FLINGER);
80    this.windowManagerTrace = traces.getTrace(TraceType.WINDOW_MANAGER);
81  }
82
83  protected override async initializeTraceSpecificData() {
84    if (this.surfaceFlingerTrace) {
85      const layersIdAndName = await this.surfaceFlingerTrace.customQuery(
86        CustomQueryType.SF_LAYERS_ID_AND_NAME,
87      );
88      layersIdAndName.forEach((value) => {
89        this.layerIdToName.set(value.id, value.name);
90      });
91    }
92
93    if (this.windowManagerTrace) {
94      const windowsTokenAndTitle = await this.windowManagerTrace.customQuery(
95        CustomQueryType.WM_WINDOWS_TOKEN_AND_TITLE,
96      );
97      windowsTokenAndTitle.forEach((value) => {
98        this.windowTokenToTitle.set(value.token, value.title);
99      });
100    }
101  }
102
103  protected override makeHeaders(): LogHeader[] {
104    return [
105      new LogHeader(Presenter.COLUMNS.id),
106      new LogHeader(
107        Presenter.COLUMNS.type,
108        new LogSelectFilter([], false, '175'),
109      ),
110      new LogHeader(Presenter.COLUMNS.sendTime),
111      new LogHeader(Presenter.COLUMNS.dispatchTime),
112      new LogHeader(Presenter.COLUMNS.duration),
113      new LogHeader(
114        Presenter.COLUMNS.handler,
115        new LogSelectFilter([], false, '250'),
116      ),
117      new LogHeader(
118        Presenter.COLUMNS.participants,
119        new LogSelectFilter([], true, '250', '100%'),
120      ),
121      new LogHeader(
122        Presenter.COLUMNS.flags,
123        new LogSelectFilter([], true, '250', '100%'),
124      ),
125      new LogHeader(Presenter.COLUMNS.status, new LogSelectFilter([])),
126    ];
127  }
128
129  protected override async makeUiDataEntries(
130    headers: LogHeader[],
131  ): Promise<TransitionsEntry[]> {
132    // TODO(b/339191691): Ideally we should refactor the parsers to
133    // keep a map of time -> rowId, instead of relying on table order
134    headers.forEach((header) => {
135      if (!header.filter) return;
136      this.uniqueFieldValues.set(header.spec, new Set());
137    });
138    const transitions = await this.makeTransitions(headers);
139    this.sortTransitions(transitions);
140    return transitions;
141  }
142
143  protected override updateFiltersInHeaders(headers: LogHeader[]) {
144    headers.forEach((header) => {
145      if (!(header.filter instanceof LogSelectFilter)) return;
146      header.filter.options = this.getUniqueUiDataEntryValues(header.spec);
147    });
148  }
149
150  private getUniqueUiDataEntryValues(spec: ColumnSpec): string[] {
151    const result = [...assertDefined(this.uniqueFieldValues.get(spec))];
152
153    result.sort((a, b) => {
154      const aIsNumber = !isNaN(Number(a));
155      const bIsNumber = !isNaN(Number(b));
156
157      if (aIsNumber && bIsNumber) {
158        return Number(a) - Number(b);
159      } else if (aIsNumber) {
160        return 1; // place number after strings in the result
161      } else if (bIsNumber) {
162        return -1; // place number after strings in the result
163      }
164
165      // a and b are both strings
166      if (a < b) {
167        return -1;
168      } else if (a > b) {
169        return 1;
170      } else {
171        return 0;
172      }
173    });
174
175    return result;
176  }
177
178  private sortTransitions(transitions: TransitionsEntry[]) {
179    const getId = (a: TransitionsEntry) =>
180      assertDefined(
181        a.fields.find((field) => field.spec === Presenter.COLUMNS.id),
182      ).value;
183    transitions.sort((a: TransitionsEntry, b: TransitionsEntry) => {
184      return getId(a) <= getId(b) ? -1 : 1;
185    });
186  }
187
188  private async makeTransitions(
189    headers: LogHeader[],
190  ): Promise<TransitionsEntry[]> {
191    const transitions: TransitionsEntry[] = [];
192    for (
193      let traceIndex = 0;
194      traceIndex < this.transitionTrace.lengthEntries;
195      ++traceIndex
196    ) {
197      const entry = assertDefined(this.trace.getEntry(traceIndex));
198      let transitionNode: PropertyTreeNode;
199      try {
200        transitionNode = await entry.getValue();
201      } catch (e) {
202        continue;
203      }
204      const wmDataNode = assertDefined(transitionNode.getChildByName('wmData'));
205      const shellDataNode = assertDefined(
206        transitionNode.getChildByName('shellData'),
207      );
208      this.updateTransitionChangesNames.apply(transitionNode);
209
210      const transitionType = this.extractAndFormatTransitionType(wmDataNode);
211      const handler = this.extractAndFormatHandler(shellDataNode);
212      const participants = this.extractAndFormatParticipants(wmDataNode);
213      const flags = this.extractAndFormatFlags(wmDataNode);
214      const [status, statusIcon, statusIconColor] =
215        this.extractAndFormatStatus(transitionNode);
216
217      const fields: LogField[] = [
218        {
219          spec: Presenter.COLUMNS.id,
220          value: assertDefined(transitionNode.getChildByName('id')).getValue(),
221        },
222        {spec: Presenter.COLUMNS.type, value: transitionType},
223        {
224          spec: Presenter.COLUMNS.sendTime,
225          value:
226            wmDataNode.getChildByName('sendTimeNs')?.getValue() ??
227            Presenter.VALUE_NA,
228        },
229        {
230          spec: Presenter.COLUMNS.dispatchTime,
231          value:
232            shellDataNode.getChildByName('dispatchTimeNs')?.getValue() ??
233            Presenter.VALUE_NA,
234          propagateEntryTimestamp: true,
235        },
236        {
237          spec: Presenter.COLUMNS.duration,
238          value:
239            transitionNode.getChildByName('duration')?.formattedValue() ??
240            Presenter.VALUE_NA,
241        },
242        {spec: Presenter.COLUMNS.handler, value: handler},
243        {spec: Presenter.COLUMNS.participants, value: participants},
244        {spec: Presenter.COLUMNS.flags, value: flags},
245        {
246          spec: Presenter.COLUMNS.status,
247          value: status,
248          icon: statusIcon,
249          iconColor: statusIconColor,
250        },
251      ];
252      transitions.push(new TransitionsEntry(entry, fields, transitionNode));
253    }
254
255    return transitions;
256  }
257
258  private extractAndFormatTransitionType(wmDataNode: PropertyTreeNode): string {
259    const transitionType =
260      wmDataNode.getChildByName('type')?.formattedValue() ?? 'NONE';
261    assertDefined(this.uniqueFieldValues.get(Presenter.COLUMNS.type)).add(
262      transitionType,
263    );
264    return transitionType;
265  }
266
267  private extractAndFormatHandler(shellDataNode: PropertyTreeNode): string {
268    const handler =
269      shellDataNode.getChildByName('handler')?.formattedValue() ??
270      Presenter.VALUE_NA;
271    assertDefined(this.uniqueFieldValues.get(Presenter.COLUMNS.handler)).add(
272      handler,
273    );
274    return handler;
275  }
276
277  private extractAndFormatFlags(wmDataNode: PropertyTreeNode): string {
278    const flags =
279      wmDataNode.getChildByName('flags')?.formattedValue() ??
280      Presenter.VALUE_NA;
281
282    const uniqueFlags = assertDefined(
283      this.uniqueFieldValues.get(Presenter.COLUMNS.flags),
284    );
285    flags
286      .split('|')
287      .map((flag) => flag.trim())
288      .forEach((flag) => uniqueFlags.add(flag));
289
290    return flags;
291  }
292
293  private extractAndFormatParticipants(wmDataNode: PropertyTreeNode): string {
294    const targets = wmDataNode.getChildByName('targets')?.getAllChildren();
295    if (!targets) return Presenter.VALUE_NA;
296
297    const layers = new Set<string>();
298    const windows = new Set<string>();
299    targets.forEach((target) => {
300      const layerId = target.getChildByName('layerId');
301      if (layerId) layers.add(layerId.formattedValue());
302      const windowId = target.getChildByName('windowId');
303      if (windowId) windows.add(windowId.formattedValue());
304    });
305    const uniqueParticipants = assertDefined(
306      this.uniqueFieldValues.get(Presenter.COLUMNS.participants),
307    );
308    layers.forEach((layer) => uniqueParticipants.add(layer));
309    windows.forEach((window) => uniqueParticipants.add(window));
310
311    return `Layers: ${
312      layers.size > 0 ? [...layers].join(', ') : Presenter.VALUE_NA
313    }\nWindows: ${
314      windows.size > 0 ? [...windows].join(', ') : Presenter.VALUE_NA
315    }`;
316  }
317
318  private extractAndFormatStatus(
319    transitionNode: PropertyTreeNode,
320  ): [string, string | undefined, string | undefined] {
321    let status = Presenter.VALUE_NA;
322    let statusIcon: string | undefined;
323    let statusIconColor: string | undefined;
324    if (assertDefined(transitionNode.getChildByName('merged')).getValue()) {
325      status = TransitionStatus.MERGED;
326      statusIcon = 'merge';
327      statusIconColor = 'gray';
328    } else if (
329      assertDefined(transitionNode.getChildByName('aborted')).getValue()
330    ) {
331      status = TransitionStatus.ABORTED;
332      statusIcon = 'close';
333      statusIconColor = 'red';
334    } else if (
335      assertDefined(transitionNode.getChildByName('played')).getValue()
336    ) {
337      status = TransitionStatus.PLAYED;
338      statusIcon = 'check';
339      statusIconColor = 'green';
340    }
341    assertDefined(this.uniqueFieldValues.get(Presenter.COLUMNS.status)).add(
342      status,
343    );
344    return [status, statusIcon, statusIconColor];
345  }
346}
347