1<!-- Copyright (C) 2020 The Android Open Source Project
2
3     Licensed under the Apache License, Version 2.0 (the "License");
4     you may not use this file except in compliance with the License.
5     You may obtain a copy of the License at
6
7          http://www.apache.org/licenses/LICENSE-2.0
8
9     Unless required by applicable law or agreed to in writing, software
10     distributed under the License is distributed on an "AS IS" BASIS,
11     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12     See the License for the specific language governing permissions and
13     limitations under the License.
14-->
15<template>
16  <md-card-content class="container">
17
18    <flat-card class="changes card">
19      <md-content
20        md-tag="md-toolbar"
21        md-elevation="0"
22        class="card-toolbar md-transparent md-dense"
23      >
24        <h2 class="md-title" style="flex: 1">Transactions</h2>
25      </md-content>
26      <div class="filters">
27        <div class="input">
28          <md-field>
29            <label>Transaction Type</label>
30            <md-select v-model="selectedTransactionTypes" multiple>
31              <md-option
32                v-for="type in transactionTypes"
33                :value="type"
34                v-bind:key="type">
35                {{ type }}
36              </md-option>
37            </md-select>
38          </md-field>
39        </div>
40
41        <div class="input">
42          <div>
43            <md-autocomplete
44              v-model="selectedProperty"
45              :md-options="properties"
46            >
47              <label>Changed property</label>
48            </md-autocomplete>
49            <!-- TODO(b/159582192): Add way to select value a property has
50              changed to, figure out how to handle properties that are
51              objects... -->
52          </div>
53        </div>
54
55        <div class="input">
56          <md-field>
57            <label>Origin PID</label>
58            <md-select v-model="selectedPids" multiple>
59              <md-option v-for="pid in pids" :value="pid" v-bind:key="pid">
60                {{ pid }}
61              </md-option>
62            </md-select>
63          </md-field>
64        </div>
65
66        <div class="input">
67          <md-field>
68            <label>Origin UID</label>
69            <md-select v-model="selectedUids" multiple>
70              <md-option v-for="uid in uids" :value="uid" v-bind:key="uid">
71                {{ uid }}
72              </md-option>
73            </md-select>
74          </md-field>
75        </div>
76
77        <div class="input">
78          <md-chips
79            v-model="filters"
80            md-placeholder="Add surface id or name..."
81          >
82            <div class="md-helper-text">Press enter to add</div>
83          </md-chips>
84        </div>
85
86        <md-checkbox v-model="trace.simplifyNames">
87            Simplify names
88        </md-checkbox>
89
90      </div>
91
92      <virtual-list style="height: 600px; overflow-y: auto;"
93        :data-key="'timestamp'"
94        :data-sources="filteredData"
95        :data-component="transactionEntryComponent"
96        :extra-props="{
97          onClick: transactionSelected,
98          selectedTransaction,
99          transactionsTrace,
100          prettifyTransactionId,
101          simplifyNames: trace.simplifyNames,
102        }"
103        ref="loglist"
104      />
105    </flat-card>
106
107    <flat-card class="changes card">
108      <md-content
109        md-tag="md-toolbar"
110        md-elevation="0"
111        class="card-toolbar md-transparent md-dense"
112      >
113        <h2 class="md-title" style="flex: 1">Changes</h2>
114      </md-content>
115      <div class="changes-content" v-if="selectedTree">
116        <div
117          v-if="selectedTransaction.type === 'transaction'"
118          class="transaction-events"
119        >
120          <div
121            v-for="(event, i) in transactionHistory(selectedTransaction)"
122            v-bind:key="`${selectedTransaction.identifier}-${i}`"
123            class="transaction-event"
124          >
125            <div v-if="event.type === 'apply'" class="applied-event">
126              applied
127            </div>
128            <div v-if="event.type === 'merge'" class="merged-event">
129              <!-- eslint-disable-next-line max-len -->
130              {{ prettifyTransactionId(event.mergedId) }}
131            </div>
132          </div>
133        </div>
134        <tree-view
135          :item="selectedTree"
136          :collapseChildren="true"
137          :useGlobalCollapsedState="true"
138        />
139      </div>
140      <div class="no-properties" v-else>
141        <i class="material-icons none-icon">
142          filter_none
143        </i>
144        <span>No transaction selected.</span>
145      </div>
146    </flat-card>
147
148  </md-card-content>
149</template>
150<script>
151import TreeView from './TreeView.vue';
152import VirtualList from '../libs/virtualList/VirtualList';
153import TransactionEntry from './TransactionEntry.vue';
154import FlatCard from './components/FlatCard.vue';
155
156import {ObjectTransformer} from './transform.js';
157import {expandTransactionId} from '@/traces/Transactions.ts';
158
159export default {
160  name: 'transactionsview',
161  props: ['trace'],
162  data() {
163    const transactionTypes = new Set();
164    const properties = new Set();
165    const pids = new Set();
166    const uids = new Set();
167    const transactionsTrace = this.trace;
168    for (const entry of transactionsTrace.data) {
169      if (entry.type == 'transaction') {
170        for (const transaction of entry.transactions) {
171          transactionTypes.add(transaction.type);
172          Object.keys(transaction.obj).forEach((item) => properties.add(item));
173        }
174      } else {
175        transactionTypes.add(entry.type);
176        Object.keys(entry.obj).forEach((item) => properties.add(item));
177      }
178
179      if (entry.origin) {
180        pids.add(entry.origin.pid);
181        uids.add(entry.origin.uid);
182      }
183    }
184
185    // Remove vsync from being transaction types that can be filtered
186    // We want to always show vsyncs
187    transactionTypes.delete('vsyncEvent');
188
189    return {
190      transactionTypes: Array.from(transactionTypes),
191      properties: Array.from(properties),
192      pids: Array.from(pids),
193      uids: Array.from(uids),
194      selectedTransactionTypes: [],
195      selectedPids: [],
196      selectedUids: [],
197      searchInput: '',
198      selectedTree: null,
199      filters: [],
200      selectedProperty: null,
201      selectedTransaction: null,
202      transactionEntryComponent: TransactionEntry,
203      transactionsTrace,
204      expandTransactionId,
205    };
206  },
207  computed: {
208    data() {
209      return this.transactionsTrace.data;
210    },
211    filteredData() {
212      let filteredData = this.data;
213
214      if (this.selectedTransactionTypes.length > 0) {
215        filteredData = filteredData.filter(
216            this.filterTransactions((transaction) =>
217              transaction.type === 'vsyncEvent' ||
218              this.selectedTransactionTypes.includes(transaction.type)));
219      }
220
221      if (this.selectedPids.length > 0) {
222        filteredData = filteredData.filter((entry) =>
223          this.selectedPids.includes(entry.origin?.pid));
224      }
225
226      if (this.selectedUids.length > 0) {
227        filteredData = filteredData.filter((entry) =>
228          this.selectedUids.includes(entry.origin?.uid));
229      }
230
231      if (this.filters.length > 0) {
232        filteredData = filteredData.filter(
233            this.filterTransactions((transaction) => {
234              for (const filter of this.filters) {
235                if (isNaN(filter) && transaction.layerName?.includes(filter)) {
236                // If filter isn't a number then check if the transaction's
237                // target surface's name matches the filter — if so keep it.
238                  return true;
239                }
240                if (filter == transaction.obj.id) {
241                // If filteter is a number then check if the filter matches
242                // the transaction's target surface id — if so keep it.
243                  return true;
244                }
245              }
246
247              // Exclude transaction if it fails to match filter.
248              return false;
249            }),
250        );
251      }
252
253      if (this.selectedProperty) {
254        filteredData = filteredData.filter(
255            this.filterTransactions((transaction) => {
256              for (const key in transaction.obj) {
257                if (this.isMeaningfulChange(transaction.obj, key) &&
258                    key === this.selectedProperty) {
259                  return true;
260                }
261              }
262
263              return false;
264            }),
265        );
266      }
267
268      // We quish vsyncs because otherwise the lazy list will not load enough
269      // elements if there are many vsyncs in a row since vsyncs take up no
270      // space.
271      return this.squishVSyncs(filteredData);
272    },
273
274  },
275  methods: {
276    removeNullFields(changeObject) {
277      for (const key in changeObject) {
278        if (changeObject[key] === null) {
279          delete changeObject[key];
280        }
281      }
282
283      return changeObject;
284    },
285    transactionSelected(transaction) {
286      this.selectedTransaction = transaction;
287
288      const META_DATA_KEY = 'metadata';
289
290      let obj;
291      let name;
292      if (transaction.type == 'transaction') {
293        name = 'changes';
294        obj = {};
295
296        const [surfaceChanges, displayChanges] =
297          this.aggregateTransactions(transaction.transactions);
298
299        // Prepare the surface and display changes to be passed through
300        // the ObjectTransformer — in particular, remove redundant properties
301        // and add metadata that can be accessed post transformation
302        const perpareForTreeViewTransform = (change) => {
303          this.removeNullFields(change);
304          change[META_DATA_KEY] = {
305            layerName: change.layerName,
306          };
307          // remove redundant properties
308          delete change.layerName;
309          delete change.id;
310        };
311
312        for (const changeId in surfaceChanges) {
313          if (surfaceChanges.hasOwnProperty(changeId)) {
314            perpareForTreeViewTransform(surfaceChanges[changeId]);
315          }
316        }
317        for (const changeId in displayChanges) {
318          if (displayChanges.hasOwnProperty(changeId)) {
319            perpareForTreeViewTransform(displayChanges[changeId]);
320          }
321        }
322
323        if (Object.keys(surfaceChanges).length > 0) {
324          obj.surfaceChanges = surfaceChanges;
325        }
326
327        if (Object.keys(displayChanges).length > 0) {
328          obj.displayChanges = displayChanges;
329        }
330      } else {
331        obj = this.removeNullFields(transaction.obj);
332        name = transaction.type;
333      }
334
335      // Transform the raw JS object to be TreeView compatible
336      const transactionUniqueId = transaction.timestamp;
337      let tree = new ObjectTransformer(
338          obj,
339          name,
340          transactionUniqueId,
341      ).setOptions({
342        formatter: () => {},
343      }).transform({
344        keepOriginal: true,
345        metadataKey: META_DATA_KEY,
346        freeze: false,
347      });
348
349      // Add the layer name as the kind of the object to be shown in the
350      // TreeView
351      const addLayerNameAsKind = (tree) => {
352        for (const layerChanges of tree.children) {
353          layerChanges.kind = layerChanges.metadata.layerName;
354        }
355      };
356
357      if (transaction.type == 'transaction') {
358        for (const child of tree.children) {
359          // child = surfaceChanges or displayChanges tree node
360          addLayerNameAsKind(child);
361        }
362      }
363
364      // If there are only surfaceChanges or only displayChanges and not both
365      // remove the extra top layer node which is meant to hold both types of
366      // changes when both are present
367      if (tree.name == 'changes' && tree.children.length === 1) {
368        tree = tree.children[0];
369      }
370
371      this.selectedTree = tree;
372    },
373    filterTransactions(condition) {
374      return (entry) => {
375        if (entry.type == 'transaction') {
376          for (const transaction of entry.transactions) {
377            if (condition(transaction)) {
378              return true;
379            }
380          }
381
382          return false;
383        } else {
384          return condition(entry);
385        }
386      };
387    },
388    isMeaningfulChange(object, key) {
389      // TODO (b/159799733): Handle cases of non null objects but meaningless
390      // change
391      return object[key] !== null && object.hasOwnProperty(key);
392    },
393    mergeChanges(a, b) {
394      const res = {};
395
396      for (const key in a) {
397        if (this.isMeaningfulChange(a, key)) {
398          res[key] = a[key];
399        }
400      }
401
402      for (const key in b) {
403        if (this.isMeaningfulChange(b, key)) {
404          if (res.hasOwnProperty(key) && key != 'id') {
405            throw new Error(`Merge failed – key '${key}' already present`);
406          }
407          res[key] = b[key];
408        }
409      }
410
411      return res;
412    },
413    aggregateTransactions(transactions) {
414      const surfaceChanges = {};
415      const displayChanges = {};
416
417      for (const transaction of transactions) {
418        const obj = transaction.obj;
419
420        // Create a new base object to merge all changes into
421        const newBaseObj = () => {
422          return {
423            layerName: transaction.layerName,
424          };
425        };
426
427        switch (transaction.type) {
428          case 'surfaceChange':
429            surfaceChanges[obj.id] =
430              this.mergeChanges(surfaceChanges[obj.id] ?? newBaseObj(), obj);
431            break;
432
433          case 'displayChange':
434            displayChanges[obj.id] =
435              this.mergeChanges(displayChanges[obj.id] ?? newBaseObj(), obj);
436            break;
437
438          default:
439            throw new Error(`Unhandled transaction type ${transaction.type}`);
440        }
441      }
442
443      return [surfaceChanges, displayChanges];
444    },
445
446    transactionHistory(selectedTransaction) {
447      const transactionId = selectedTransaction.identifier;
448      const history = this.transactionsTrace.transactionHistory
449          .generateHistoryTreesOf(transactionId);
450
451      return history;
452    },
453
454    prettifyTransactionId(transactionId) {
455      const expandedId = expandTransactionId(transactionId);
456      return `${expandedId.pid}.${expandedId.id}`;
457    },
458
459    squishVSyncs(data) {
460      return data.filter((event, i) => {
461        return !(event.type === 'vsyncEvent' &&
462          data[i + 1]?.type === 'vsyncEvent');
463      });
464    },
465  },
466  components: {
467    'virtual-list': VirtualList,
468    'tree-view': TreeView,
469    'flat-card': FlatCard,
470  },
471};
472
473</script>
474<style scoped>
475.container {
476  display: flex;
477  flex-wrap: wrap;
478}
479
480.transaction-table,
481.changes {
482  flex: 1 1 0;
483  width: 0;
484  margin: 8px;
485}
486
487.scrollBody {
488  width: 100%;
489  height: 100%;
490  overflow: scroll;
491}
492
493.filters {
494  margin-bottom: 15px;
495  width: 100%;
496  padding: 15px 5px;
497  display: flex;
498  flex-wrap: wrap;
499}
500
501.filters .input {
502  max-width: 300px;
503  margin: 0 10px;
504  flex-grow: 1;
505}
506
507.changes-content {
508  padding: 18px;
509  height: 550px;
510  overflow: auto;
511}
512
513.no-properties {
514  display: flex;
515  flex-direction: column;
516  align-self: center;
517  align-items: center;
518  justify-content: center;
519  height: calc(100% - 50px);
520  padding: 50px 25px;
521}
522
523.no-properties .none-icon {
524  font-size: 35px;
525  margin-bottom: 10px;
526}
527
528.no-properties span {
529  font-weight: 100;
530}
531
532.transaction-event {
533  display: inline-flex;
534}
535</style>
536