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 {ArrayUtils} from 'common/array_utils';
18import {assertDefined} from 'common/assert_utils';
19import {FunctionUtils} from 'common/function_utils';
20import {PersistentStoreProxy} from 'common/persistent_store_proxy';
21import {
22  TracePositionUpdate,
23  WinscopeEvent,
24  WinscopeEventType,
25} from 'messaging/winscope_event';
26import {
27  EmitEvent,
28  WinscopeEventEmitter,
29} from 'messaging/winscope_event_emitter';
30import {AbsoluteEntryIndex} from 'trace/index_types';
31import {Trace, TraceEntry} from 'trace/trace';
32import {TraceEntryFinder} from 'trace/trace_entry_finder';
33import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
34import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
35import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from 'trace/tree_node/property_tree_node_factory';
36import {Filter} from 'viewers/common/operations/filter';
37import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
38import {UiTreeFormatter} from 'viewers/common/ui_tree_formatter';
39import {UiTreeUtils} from 'viewers/common/ui_tree_utils';
40import {UserOptions} from 'viewers/common/user_options';
41import {SetRootDisplayNames} from './operations/set_root_display_name';
42import {UiData, UiDataEntry, UiDataEntryType} from './ui_data';
43
44type NotifyViewCallbackType = (uiData: UiData) => void;
45
46export class Presenter implements WinscopeEventEmitter {
47  private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
48  private readonly trace: Trace<PropertyTreeNode>;
49  private entry?: TraceEntry<PropertyTreeNode>;
50  private originalIndicesOfUiDataEntries: number[];
51  private uiData = UiData.EMPTY;
52
53  private isInitialized = false;
54  private allUiDataEntries: UiDataEntry[] = [];
55  private allVSyncIds: string[] = [];
56  private allPids: string[] = [];
57  private allUids: string[] = [];
58  private allTypes: string[] = [];
59  private allLayerAndDisplayIds: string[] = [];
60  private allTransactionIds: string[] = [];
61  private allFlags: string[] = [];
62
63  private vsyncIdFilter: string[] = [];
64  private pidFilter: string[] = [];
65  private uidFilter: string[] = [];
66  private typeFilter: string[] = [];
67  private layerIdFilter: string[] = [];
68  private whatFilter: string[] = [];
69  private transactionIdFilter: string[] = [];
70
71  private currentPropertiesTree: PropertyTreeNode | undefined;
72
73  private propertiesUserOptions: UserOptions =
74    PersistentStoreProxy.new<UserOptions>(
75      'TransactionsPropertyOptions',
76      {
77        showDefaults: {
78          name: 'Show defaults',
79          enabled: false,
80          tooltip: `
81                If checked, shows the value of all properties.
82                Otherwise, hides all properties whose value is
83                the default for its data type.
84              `,
85        },
86      },
87      this.storage,
88    );
89
90  private readonly notifyUiDataCallback: NotifyViewCallbackType;
91  private static readonly VALUE_NA = 'N/A';
92
93  constructor(
94    trace: Trace<PropertyTreeNode>,
95    private readonly storage: Storage,
96    notifyViewCallback: NotifyViewCallbackType,
97  ) {
98    this.trace = trace;
99    this.notifyUiDataCallback = notifyViewCallback;
100    this.originalIndicesOfUiDataEntries = [];
101    this.notifyUiDataCallback(this.uiData);
102  }
103
104  setEmitEvent(callback: EmitEvent) {
105    this.emitAppEvent = callback;
106  }
107
108  async onAppEvent(event: WinscopeEvent) {
109    await event.visit(
110      WinscopeEventType.TRACE_POSITION_UPDATE,
111      async (event) => {
112        await this.initializeIfNeeded();
113        this.entry = TraceEntryFinder.findCorrespondingEntry(
114          this.trace,
115          event.position,
116        );
117        this.uiData.currentEntryIndex = this.computeCurrentEntryIndex();
118        this.uiData.selectedEntryIndex = undefined;
119        this.uiData.scrollToIndex = this.uiData.currentEntryIndex;
120        this.currentPropertiesTree = this.computePropertiesTree(
121          this.uiData.entries,
122          this.uiData.currentEntryIndex,
123          this.uiData.selectedEntryIndex,
124        );
125        this.uiData.currentPropertiesTree = this.formatPropertiesTree(
126          this.currentPropertiesTree,
127        );
128
129        this.notifyUiDataCallback(this.uiData);
130      },
131    );
132  }
133
134  onVSyncIdFilterChanged(vsyncIds: string[]) {
135    this.vsyncIdFilter = vsyncIds;
136    this.uiData = this.computeUiData();
137    this.notifyUiDataCallback(this.uiData);
138  }
139
140  onPidFilterChanged(pids: string[]) {
141    this.pidFilter = pids;
142    this.uiData = this.computeUiData();
143    this.notifyUiDataCallback(this.uiData);
144  }
145
146  onUidFilterChanged(uids: string[]) {
147    this.uidFilter = uids;
148    this.uiData = this.computeUiData();
149    this.notifyUiDataCallback(this.uiData);
150  }
151
152  onTypeFilterChanged(types: string[]) {
153    this.typeFilter = types;
154    this.uiData = this.computeUiData();
155    this.notifyUiDataCallback(this.uiData);
156  }
157
158  onLayerIdFilterChanged(ids: string[]) {
159    this.layerIdFilter = ids;
160    this.uiData = this.computeUiData();
161    this.notifyUiDataCallback(this.uiData);
162  }
163
164  onWhatFilterChanged(flags: string[]) {
165    this.whatFilter = flags;
166    this.uiData = this.computeUiData();
167    this.notifyUiDataCallback(this.uiData);
168  }
169
170  onTransactionIdFilterChanged(ids: string[]) {
171    this.transactionIdFilter = ids;
172    this.uiData = this.computeUiData();
173    this.notifyUiDataCallback(this.uiData);
174  }
175
176  onEntryClicked(index: number) {
177    if (this.uiData.selectedEntryIndex === index) {
178      return;
179    }
180    this.uiData.selectedEntryIndex = index;
181    this.uiData.scrollToIndex = undefined; // no scrolling
182    this.updatePropertiesTree();
183  }
184
185  onEntryChangedByKeyPress(index: number) {
186    if (this.uiData.selectedEntryIndex === index) {
187      return;
188    }
189    this.uiData.selectedEntryIndex = index;
190    this.uiData.scrollToIndex = index;
191    this.updatePropertiesTree();
192  }
193
194  onPropertiesUserOptionsChange(userOptions: UserOptions) {
195    this.propertiesUserOptions = userOptions;
196    this.uiData.propertiesUserOptions = this.propertiesUserOptions;
197    this.uiData.currentPropertiesTree = this.formatPropertiesTree(
198      this.currentPropertiesTree,
199    );
200    this.notifyUiDataCallback(this.uiData);
201  }
202
203  async onLogTimestampClicked(traceIndex: AbsoluteEntryIndex) {
204    await this.emitAppEvent(
205      TracePositionUpdate.fromTraceEntry(this.trace.getEntry(traceIndex), true),
206    );
207  }
208
209  private async initializeIfNeeded() {
210    if (this.isInitialized) {
211      return;
212    }
213
214    this.allUiDataEntries = await this.makeUiDataEntries();
215
216    this.allVSyncIds = this.getUniqueUiDataEntryValues(
217      this.allUiDataEntries,
218      (entry: UiDataEntry) => entry.vsyncId.toString(),
219    );
220    this.allPids = this.getUniqueUiDataEntryValues(
221      this.allUiDataEntries,
222      (entry: UiDataEntry) => entry.pid,
223    );
224    this.allUids = this.getUniqueUiDataEntryValues(
225      this.allUiDataEntries,
226      (entry: UiDataEntry) => entry.uid,
227    );
228    this.allTypes = this.getUniqueUiDataEntryValues(
229      this.allUiDataEntries,
230      (entry: UiDataEntry) => entry.type,
231    );
232    this.allLayerAndDisplayIds = this.getUniqueUiDataEntryValues(
233      this.allUiDataEntries,
234      (entry: UiDataEntry) => entry.layerOrDisplayId,
235    );
236    this.allTransactionIds = this.getUniqueUiDataEntryValues(
237      this.allUiDataEntries,
238      (entry: UiDataEntry) => entry.transactionId,
239    );
240    this.allFlags = this.getUniqueUiDataEntryValues(
241      this.allUiDataEntries,
242      (entry: UiDataEntry) => entry.what.split('|').map((flag) => flag.trim()),
243    );
244
245    this.uiData = this.computeUiData();
246
247    this.isInitialized = true;
248  }
249
250  private computeUiData(): UiData {
251    const entries = this.allUiDataEntries;
252
253    let filteredEntries = entries;
254
255    if (this.vsyncIdFilter.length > 0) {
256      filteredEntries = filteredEntries.filter((entry) =>
257        this.vsyncIdFilter.includes(entry.vsyncId.toString()),
258      );
259    }
260
261    if (this.pidFilter.length > 0) {
262      filteredEntries = filteredEntries.filter((entry) =>
263        this.pidFilter.includes(entry.pid),
264      );
265    }
266
267    if (this.uidFilter.length > 0) {
268      filteredEntries = filteredEntries.filter((entry) =>
269        this.uidFilter.includes(entry.uid),
270      );
271    }
272
273    if (this.typeFilter.length > 0) {
274      filteredEntries = filteredEntries.filter((entry) =>
275        this.typeFilter.includes(entry.type),
276      );
277    }
278
279    if (this.layerIdFilter.length > 0) {
280      filteredEntries = filteredEntries.filter((entry) =>
281        this.layerIdFilter.includes(entry.layerOrDisplayId),
282      );
283    }
284
285    if (this.whatFilter.length > 0) {
286      filteredEntries = filteredEntries.filter(
287        (entry) =>
288          this.whatFilter.find((flag) => entry.what.includes(flag)) !==
289          undefined,
290      );
291    }
292
293    if (this.transactionIdFilter.length > 0) {
294      filteredEntries = filteredEntries.filter((entry) =>
295        this.transactionIdFilter.includes(entry.transactionId.toString()),
296      );
297    }
298
299    this.originalIndicesOfUiDataEntries = filteredEntries.map(
300      (entry) => entry.traceIndex,
301    );
302
303    const currentEntryIndex = this.computeCurrentEntryIndex();
304    const selectedEntryIndex = undefined;
305    this.currentPropertiesTree = this.computePropertiesTree(
306      filteredEntries,
307      currentEntryIndex,
308      selectedEntryIndex,
309    );
310
311    const formattedPropertiesTree = this.formatPropertiesTree(
312      this.currentPropertiesTree,
313    );
314
315    return new UiData(
316      this.allVSyncIds,
317      this.allPids,
318      this.allUids,
319      this.allTypes,
320      this.allLayerAndDisplayIds,
321      this.allTransactionIds,
322      this.allFlags,
323      filteredEntries,
324      currentEntryIndex,
325      selectedEntryIndex,
326      currentEntryIndex,
327      formattedPropertiesTree,
328      this.propertiesUserOptions,
329    );
330  }
331
332  private computeCurrentEntryIndex(): undefined | number {
333    if (!this.entry) {
334      return undefined;
335    }
336
337    if (this.originalIndicesOfUiDataEntries.length === 0) {
338      return undefined;
339    }
340
341    return (
342      ArrayUtils.binarySearchFirstGreaterOrEqual(
343        this.originalIndicesOfUiDataEntries,
344        this.entry.getIndex(),
345      ) ?? this.originalIndicesOfUiDataEntries.length - 1
346    );
347  }
348
349  private updatePropertiesTree() {
350    this.currentPropertiesTree = this.computePropertiesTree(
351      this.uiData.entries,
352      this.uiData.currentEntryIndex,
353      this.uiData.selectedEntryIndex,
354    );
355
356    this.uiData.currentPropertiesTree = this.formatPropertiesTree(
357      this.currentPropertiesTree,
358    );
359
360    this.notifyUiDataCallback(this.uiData);
361  }
362
363  private computePropertiesTree(
364    entries: UiDataEntry[],
365    currentEntryIndex: undefined | number,
366    selectedEntryIndex: undefined | number,
367  ): PropertyTreeNode | undefined {
368    if (selectedEntryIndex !== undefined) {
369      return entries[selectedEntryIndex].propertiesTree;
370    }
371    if (currentEntryIndex !== undefined) {
372      return entries[currentEntryIndex].propertiesTree;
373    }
374    return undefined;
375  }
376
377  private formatPropertiesTree(
378    propertiesTree: PropertyTreeNode | undefined,
379  ): UiPropertyTreeNode | undefined {
380    if (!propertiesTree) return undefined;
381
382    const uiTree = UiPropertyTreeNode.from(propertiesTree);
383    const formatter = new UiTreeFormatter<UiPropertyTreeNode>().setUiTree(
384      uiTree,
385    );
386
387    if (!this.propertiesUserOptions['showDefaults']?.enabled) {
388      formatter.addOperation(
389        new Filter(
390          [
391            UiTreeUtils.isNotDefault,
392            UiTreeUtils.makePropertyMatchFilter('IDENTITY'),
393          ],
394          false,
395        ),
396      );
397    }
398
399    return formatter.addOperation(new SetRootDisplayNames()).format();
400  }
401
402  private async makeUiDataEntries(): Promise<UiDataEntry[]> {
403    const entries: UiDataEntry[] = [];
404
405    const entryProtos = await Promise.all(
406      this.trace.mapEntry(async (entry) => {
407        return await entry.getValue();
408      }),
409    );
410
411    for (
412      let traceIndex = 0;
413      traceIndex < this.trace.lengthEntries;
414      ++traceIndex
415    ) {
416      const entry = this.trace.getEntry(traceIndex);
417      const entryNode = entryProtos[traceIndex];
418      const vsyncId = Number(
419        assertDefined(entryNode.getChildByName('vsyncId')).getValue(),
420      );
421
422      const entryTimestamp =
423        DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty(
424          'TransactionsTraceEntry',
425          'timestamp',
426          entry.getTimestamp(),
427        );
428      entryTimestamp.setFormatter(TIMESTAMP_NODE_FORMATTER);
429
430      for (const transactionState of assertDefined(
431        entryNode.getChildByName('transactions'),
432      ).getAllChildren()) {
433        const pid = assertDefined(
434          transactionState.getChildByName('pid'),
435        ).formattedValue();
436        const uid = assertDefined(
437          transactionState.getChildByName('uid'),
438        ).formattedValue();
439        const transactionId = assertDefined(
440          transactionState.getChildByName('transactionId'),
441        ).formattedValue();
442
443        const layerChanges = assertDefined(
444          transactionState.getChildByName('layerChanges'),
445        ).getAllChildren();
446        for (const layerState of layerChanges) {
447          entries.push(
448            new UiDataEntry(
449              traceIndex,
450              entryTimestamp,
451              vsyncId,
452              pid,
453              uid,
454              UiDataEntryType.LAYER_CHANGED,
455              assertDefined(
456                layerState.getChildByName('layerId'),
457              ).formattedValue(),
458              transactionId,
459              assertDefined(layerState.getChildByName('what')).formattedValue(),
460              layerState,
461            ),
462          );
463        }
464
465        const displayChanges = assertDefined(
466          transactionState.getChildByName('displayChanges'),
467        ).getAllChildren();
468        for (const displayState of displayChanges) {
469          entries.push(
470            new UiDataEntry(
471              traceIndex,
472              entryTimestamp,
473              vsyncId,
474              pid,
475              uid,
476              UiDataEntryType.DISPLAY_CHANGED,
477              assertDefined(displayState.getChildByName('id')).formattedValue(),
478              transactionId,
479              assertDefined(
480                displayState.getChildByName('what'),
481              ).formattedValue(),
482              displayState,
483            ),
484          );
485        }
486
487        if (layerChanges.length === 0 && displayChanges.length === 0) {
488          entries.push(
489            new UiDataEntry(
490              traceIndex,
491              entryTimestamp,
492              vsyncId,
493              pid,
494              uid,
495              UiDataEntryType.NO_OP,
496              '',
497              transactionId,
498              '',
499              undefined,
500            ),
501          );
502        }
503      }
504
505      for (const layerCreationArgs of assertDefined(
506        entryNode.getChildByName('addedLayers'),
507      ).getAllChildren()) {
508        entries.push(
509          new UiDataEntry(
510            traceIndex,
511            entryTimestamp,
512            vsyncId,
513            Presenter.VALUE_NA,
514            Presenter.VALUE_NA,
515            UiDataEntryType.LAYER_ADDED,
516            assertDefined(
517              layerCreationArgs.getChildByName('layerId'),
518            ).formattedValue(),
519            '',
520            '',
521            layerCreationArgs,
522          ),
523        );
524      }
525
526      for (const destroyedLayerId of assertDefined(
527        entryNode.getChildByName('destroyedLayers'),
528      ).getAllChildren()) {
529        entries.push(
530          new UiDataEntry(
531            traceIndex,
532            entryTimestamp,
533            vsyncId,
534            Presenter.VALUE_NA,
535            Presenter.VALUE_NA,
536            UiDataEntryType.LAYER_DESTROYED,
537            destroyedLayerId.formattedValue(),
538            '',
539            '',
540            destroyedLayerId,
541          ),
542        );
543      }
544
545      for (const displayState of assertDefined(
546        entryNode.getChildByName('addedDisplays'),
547      ).getAllChildren()) {
548        entries.push(
549          new UiDataEntry(
550            traceIndex,
551            entryTimestamp,
552            vsyncId,
553            Presenter.VALUE_NA,
554            Presenter.VALUE_NA,
555            UiDataEntryType.DISPLAY_ADDED,
556            assertDefined(displayState.getChildByName('id')).formattedValue(),
557            '',
558            assertDefined(displayState.getChildByName('what')).formattedValue(),
559            displayState,
560          ),
561        );
562      }
563
564      for (const removedDisplayId of assertDefined(
565        entryNode.getChildByName('removedDisplays'),
566      ).getAllChildren()) {
567        entries.push(
568          new UiDataEntry(
569            traceIndex,
570            entryTimestamp,
571            vsyncId,
572            Presenter.VALUE_NA,
573            Presenter.VALUE_NA,
574            UiDataEntryType.DISPLAY_REMOVED,
575            removedDisplayId.formattedValue(),
576            '',
577            '',
578            removedDisplayId,
579          ),
580        );
581      }
582
583      for (const destroyedLayerHandleId of assertDefined(
584        entryNode.getChildByName('destroyedLayerHandles'),
585      ).getAllChildren()) {
586        entries.push(
587          new UiDataEntry(
588            traceIndex,
589            entryTimestamp,
590            vsyncId,
591            Presenter.VALUE_NA,
592            Presenter.VALUE_NA,
593            UiDataEntryType.LAYER_HANDLE_DESTROYED,
594            destroyedLayerHandleId.formattedValue(),
595            '',
596            '',
597            destroyedLayerHandleId,
598          ),
599        );
600      }
601    }
602
603    return entries;
604  }
605
606  private getUniqueUiDataEntryValues<T>(
607    entries: UiDataEntry[],
608    getValue: (entry: UiDataEntry) => T | T[],
609  ): T[] {
610    const uniqueValues = new Set<T>();
611    entries.forEach((entry: UiDataEntry) => {
612      const value = getValue(entry);
613      if (Array.isArray(value)) {
614        value.forEach((val) => uniqueValues.add(val));
615      } else {
616        uniqueValues.add(value);
617      }
618    });
619
620    const result = [...uniqueValues];
621
622    result.sort((a, b) => {
623      const aIsNumber = !isNaN(Number(a));
624      const bIsNumber = !isNaN(Number(b));
625
626      if (aIsNumber && bIsNumber) {
627        return Number(a) - Number(b);
628      } else if (aIsNumber) {
629        return 1; // place number after strings in the result
630      } else if (bIsNumber) {
631        return -1; // place number after strings in the result
632      }
633
634      // a and b are both strings
635      if (a < b) {
636        return -1;
637      } else if (a > b) {
638        return 1;
639      } else {
640        return 0;
641      }
642    });
643
644    return result;
645  }
646}
647