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