Add tags/errors to trace views.

At the start or end timestamp of a transition, or at an error timestamp, colour coded tags show up
next to the relevant layer/task in the WM and SF trace hierarchy views.
Tick the transition checkbox to only show entries that have tags/errors.
Compatible with flat checkbox.

Bug: b/196544612

Test: upload the zip onto the x20 (see bug) and test above.
Change-Id: Ied84adbedc38324629c8b50178b44041f8e8b66e
diff --git a/tools/winscope/src/App.vue b/tools/winscope/src/App.vue
index 291ae7a..ab88a1b 100644
--- a/tools/winscope/src/App.vue
+++ b/tools/winscope/src/App.vue
@@ -52,6 +52,8 @@
               :ref="file.type"
               :store="store"
               :file="file"
+              :presentTags="Object.freeze(presentTags)"
+              :presentErrors="Object.freeze(presentErrors)"
               @click="onDataViewFocus(file)"
             />
           </div>
@@ -102,6 +104,7 @@
         simplifyNames: true,
         displayDefaults: true,
         navigationStyle: NAVIGATION_STYLE.GLOBAL,
+        flickerTraceView: false,
       }),
       overlayRef: 'overlay',
       mainContentStyle: {
@@ -110,7 +113,6 @@
       presentTags: [],
       presentErrors: [],
       searchTypes: [SEARCH_TYPE.TIMESTAMP],
-      tagAndErrorTraces: false,
     };
   },
   created() {
@@ -124,7 +126,7 @@
   },
 
   methods: {
-    /** get states from either tag files or error files */
+    /** Get states from either tag files or error files */
     getUpdatedStates(files) {
       var states = [];
       for (const file of files) {
@@ -132,7 +134,7 @@
       }
       return states;
     },
-    /** get tags from all uploaded tag files*/
+    /** Get tags from all uploaded tag files*/
     getUpdatedTags() {
       var tagStates = this.getUpdatedStates(this.tagFiles);
       var tags = [];
@@ -144,7 +146,7 @@
       });
       return tags;
     },
-    /** get tags from all uploaded error files*/
+    /** Get tags from all uploaded error files*/
     getUpdatedErrors() {
       var errorStates = this.getUpdatedStates(this.errorFiles);
       var errors = [];
@@ -157,11 +159,11 @@
       });
       return errors;
     },
-    /** set flicker mode check for if there are tag/error traces uploaded*/
+    /** Set flicker mode check for if there are tag/error traces uploaded*/
     shouldUpdateTagAndErrorTraces() {
       return this.tagFiles.length > 0 || this.errorFiles.length > 0;
     },
-    /** activate flicker search tab if tags/errors uploaded*/
+    /** Activate flicker search tab if tags/errors uploaded*/
     updateSearchTypes() {
       this.searchTypes = [SEARCH_TYPE.TIMESTAMP];
       if (this.tagAndErrorTraces) this.searchTypes.push(SEARCH_TYPE.TAG);
diff --git a/tools/winscope/src/DataView.vue b/tools/winscope/src/DataView.vue
index db3c07f..7db5699 100644
--- a/tools/winscope/src/DataView.vue
+++ b/tools/winscope/src/DataView.vue
@@ -40,12 +40,16 @@
         v-if="showInWindowManagerTraceView(file)"
         :store="store"
         :file="file"
+        :presentTags="presentTags"
+        :presentErrors="presentErrors"
         ref="view"
       />
       <SurfaceFlingerTraceView
         v-else-if="showInSurfaceFlingerTraceView(file)"
         :store="store"
         :file="file"
+        :presentTags="presentTags"
+        :presentErrors="presentErrors"
         ref="view"
       />
       <transactionsview
@@ -151,7 +155,7 @@
       this.$emit('click', e);
     },
   },
-  props: ['store', 'file'],
+  props: ['store', 'file', 'presentTags', 'presentErrors'],
   mixins: [FileType],
   components: {
     'traceview': TraceView,
diff --git a/tools/winscope/src/DefaultTreeElement.vue b/tools/winscope/src/DefaultTreeElement.vue
index 1df8d17..96c7dcf 100644
--- a/tools/winscope/src/DefaultTreeElement.vue
+++ b/tools/winscope/src/DefaultTreeElement.vue
@@ -43,13 +43,28 @@
         {{c.long}}
       </md-tooltip>
     </div>
+    <div class="flicker-tags" v-for="transition in transitions" :key="transition">
+      <Arrow
+        class="transition-arrow"
+        :style="{color: transitionArrowColor(transition)}"
+      />
+      <md-tooltip md-direction="right"> {{transitionTooltip(transition)}} </md-tooltip>
+    </div>
+    <div class="flicker-tags" v-for="error in errors" :key="error.message">
+      <Arrow class="error-arrow"/>
+      <md-tooltip md-direction="right"> Error: {{error.message}} </md-tooltip>
+    </div>
   </span>
 </template>
 
 <script>
+
+import Arrow from './components/TagDisplay/Arrow.vue';
+import {transitionMap} from './utils/consts.js';
+
 export default {
   name: 'DefaultTreeElement',
-  props: ['item', 'simplify-names'],
+  props: ['item', 'simplify-names', 'errors', 'transitions'],
   methods: {
     chipClassForChip(c) {
       return [
@@ -59,6 +74,15 @@
           (c.type?.toString() || c.class?.toString() || 'default'),
       ];
     },
+    transitionArrowColor(transition) {
+      return transitionMap.get(transition).color;
+    },
+    transitionTooltip(transition) {
+      return transitionMap.get(transition).desc;
+    },
+  },
+  components: {
+    Arrow,
   },
 };
 </script>
@@ -100,4 +124,12 @@
   flex: 1 1 auto;
   width: 0;
 }
+
+.flicker-tags {
+  display: inline-block;
+}
+
+.error-arrow {
+  color: red;
+}
 </style>
diff --git a/tools/winscope/src/Searchbar.vue b/tools/winscope/src/Searchbar.vue
index 1f5fa4e..f893122 100644
--- a/tools/winscope/src/Searchbar.vue
+++ b/tools/winscope/src/Searchbar.vue
@@ -76,7 +76,7 @@
             class="inline-error"
             @click="setCurrentTimestamp(item.timestamp)"
           >
-            Error
+            Error: {{item.message}}
           </td>
         </tr>
       </table>
@@ -117,17 +117,17 @@
     };
   },
   methods: {
-    /** set search type depending on tab selected */
+    /** Set search type depending on tab selected */
     setSearchType(searchType) {
       this.searchType = searchType;
     },
-    /** set tab class to determine color highlight for active tab */
+    /** Set tab class to determine color highlight for active tab */
     tabClass(searchType) {
       var isActive = (this.searchType === searchType) ? 'active' : 'inactive';
       return ['tab', isActive];
     },
 
-    /** filter all the tags present in the trace by the searchbar input */
+    /** Filter all the tags present in the trace by the searchbar input */
     filteredTags() {
       var tags = [];
       var filter = this.searchInput.toUpperCase();
@@ -136,7 +136,7 @@
       });
       return tags;
     },
-    /** add filtered errors to filtered tags to integrate both into table*/
+    /** Add filtered errors to filtered tags to integrate both into table*/
     filteredTagsAndErrors() {
       var tagsAndErrors = [...this.filteredTags()];
       var filter = this.searchInput.toUpperCase();
@@ -148,9 +148,9 @@
 
       return tagsAndErrors;
     },
-    /** each transition has two tags present
-     * isolate the tags for the desire transition
-     * add a desc to display the timestamps as strings
+    /** Each transition has two tags present
+     * Isolate the tags for the desire transition
+     * Add a desc to display the timestamps as strings
      */
     transitionTags(id) {
       var tags = this.filteredTags().filter((tag) => tag.id === id);
@@ -160,36 +160,36 @@
       return tags;
     },
 
-    /** find the start as minimum timestamp in transition tags */
+    /** Find the start as minimum timestamp in transition tags */
     transitionStart(tags) {
       var times = tags.map((tag) => tag.timestamp);
       return times[0];
     },
-    /** find the end as maximum timestamp in transition tags */
+    /** Find the end as maximum timestamp in transition tags */
     transitionEnd(tags) {
       var times = tags.map((tag) => tag.timestamp);
       return times[times.length - 1];
     },
-    /** upon selecting a start/end tag in the dropdown;
+    /** Upon selecting a start/end tag in the dropdown;
      * navigates to that timestamp in the timeline */
     setCurrentTimestamp(timestamp) {
       this.$store.dispatch("updateTimelineTime", timestamp);
     },
 
-    /** colour codes text of transition in dropdown */
+    /** Colour codes text of transition in dropdown */
     transitionTextColor(transition) {
       return transitionMap.get(transition).color;
     },
-    /** displays transition description rather than variable name */
+    /** Displays transition description rather than variable name */
     transitionDesc(transition) {
       return transitionMap.get(transition).desc;
     },
-    /** add a desc to display the error timestamps as strings */
+    /** Add a desc to display the error timestamps as strings */
     errorDesc(timestamp) {
       return nanos_to_string(timestamp);
     },
 
-    /** navigates to closest timestamp in timeline to search input*/
+    /** Navigates to closest timestamp in timeline to search input*/
     updateSearchForTimestamp() {
       if (regExpTimestampSearch.test(this.searchInput)) {
         var roundedTimestamp = parseInt(this.searchInput);
diff --git a/tools/winscope/src/SurfaceFlingerTraceView.vue b/tools/winscope/src/SurfaceFlingerTraceView.vue
index b40aab1..5a848c1 100644
--- a/tools/winscope/src/SurfaceFlingerTraceView.vue
+++ b/tools/winscope/src/SurfaceFlingerTraceView.vue
@@ -14,7 +14,13 @@
 -->
 
 <template>
-  <TraceView :store="store" :file="file" :summarizer="summarizer" />
+  <TraceView
+    :store="store"
+    :file="file"
+    :summarizer="summarizer"
+    :presentTags="presentTags"
+    :presentErrors="presentErrors"
+  />
 </template>
 
 <script>
@@ -22,7 +28,7 @@
 
 export default {
   name: 'SurfaceFlingerTraceView',
-  props: ['store', 'file'],
+  props: ['store', 'file', 'presentTags', 'presentErrors'],
   components: {
     TraceView,
   },
diff --git a/tools/winscope/src/TraceView.vue b/tools/winscope/src/TraceView.vue
index 6c281a5..ab77d35 100644
--- a/tools/winscope/src/TraceView.vue
+++ b/tools/winscope/src/TraceView.vue
@@ -42,6 +42,7 @@
           </md-checkbox>
           <md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox>
           <md-checkbox v-model="store.flattened">Flat</md-checkbox>
+          <md-checkbox v-if="hasTagsOrErrors" v-model="store.flickerTraceView">Flicker</md-checkbox>
           <md-field md-inline class="filter">
             <label>Filter...</label>
             <md-input v-model="hierarchyPropertyFilterString"></md-input>
@@ -56,6 +57,9 @@
             :filter="hierarchyFilter"
             :flattened="store.flattened"
             :onlyVisible="store.onlyVisible"
+            :flickerTraceView="store.flickerTraceView"
+            :presentTags="presentTags"
+            :presentErrors="presentErrors"
             :items-clickable="true"
             :useGlobalCollapsedState="true"
             :simplify-names="store.simplifyNames"
@@ -166,7 +170,7 @@
 
 export default {
   name: 'traceview',
-  props: ['store', 'file', 'summarizer'],
+  props: ['store', 'file', 'summarizer', 'presentTags', 'presentErrors'],
   data() {
     return {
       propertyFilterString: '',
@@ -307,10 +311,30 @@
 
       return prevEntry;
     },
+
+    /** Performs check for id match between entry and present tags/errors
+     * must be carried out for every present tag/error
+     */
+    matchItems(flickerItems, entryItem) {
+      var match = false;
+      flickerItems.forEach(flickerItem => {
+        if (flickerItem.taskId===entryItem.taskId || flickerItem.layerId===entryItem.id) {
+          match = true;
+        }
+      });
+      return match;
+    },
+    /** Returns check for id match between entry and present tags/errors */
+    isEntryTagMatch(entryItem) {
+      return this.matchItems(this.presentTags, entryItem) || this.matchItems(this.presentErrors, entryItem);
+    },
   },
   created() {
     this.setData(this.file.data[this.file.selectedIndex ?? 0]);
   },
+  destroyed() {
+    this.store.flickerTraceView = false;
+  },
   watch: {
     selectedIndex() {
       this.setData(this.file.data[this.file.selectedIndex ?? 0]);
@@ -338,9 +362,12 @@
     hierarchyFilter() {
       const hierarchyPropertyFilter =
           getFilter(this.hierarchyPropertyFilterString);
-      return this.store.onlyVisible ? (c) => {
+      var fil = this.store.onlyVisible ? (c) => {
         return c.isVisible && hierarchyPropertyFilter(c);
       } : hierarchyPropertyFilter;
+      return this.store.flickerTraceView ? (c) => {
+        return this.isEntryTagMatch(c);
+      } : fil;
     },
     propertyFilter() {
       return getFilter(this.propertyFilterString);
@@ -364,6 +391,9 @@
 
       return summary;
     },
+    hasTagsOrErrors() {
+      return this.presentTags.length > 0 || this.presentErrors.length > 0;
+    },
   },
   components: {
     'tree-view': TreeView,
diff --git a/tools/winscope/src/TreeView.vue b/tools/winscope/src/TreeView.vue
index ff3c04f..846924e 100644
--- a/tools/winscope/src/TreeView.vue
+++ b/tools/winscope/src/TreeView.vue
@@ -50,7 +50,12 @@
           />
         </div>
         <div v-else>
-          <DefaultTreeElement :item="item" :simplify-names="simplifyNames"/>
+          <DefaultTreeElement
+            :item="item"
+            :simplify-names="simplifyNames"
+            :errors="errors"
+            :transitions="transitions"
+          />
         </div>
       </div>
       <div v-show="isCollapsed">
@@ -89,6 +94,9 @@
         :flattened="flattened"
         :onlyVisible="onlyVisible"
         :simplify-names="simplifyNames"
+        :flickerTraceView="flickerTraceView"
+        :presentTags="currentTags"
+        :presentErrors="currentErrors"
         :force-flattened="applyingFlattened"
         v-show="filterMatches(c)"
         :items-clickable="itemsClickable"
@@ -113,7 +121,6 @@
 <script>
 import DefaultTreeElement from './DefaultTreeElement.vue';
 import NodeContextMenu from './NodeContextMenu.vue';
-
 import {DiffType} from './utils/diff.js';
 
 /* in px, must be kept in sync with css, maybe find a better solution... */
@@ -143,6 +150,9 @@
     // Custom view to use to render the elements in the tree view
     'elementView',
     'onlyVisible',
+    'flickerTraceView',
+    'presentTags',
+    'presentErrors',
   ],
   data() {
     const isCollapsedByDefault = this.collapse ?? false;
@@ -163,6 +173,10 @@
         [DiffType.MODIFIED]: '.',
         [DiffType.MOVED]: '.',
       },
+      currentTags: [],
+      currentErrors: [],
+      transitions: [],
+      errors: [],
     };
   },
   watch: {
@@ -179,6 +193,10 @@
     },
     currentTimestamp() {
       // Update anything that is required to change when time changes.
+      this.currentTags = this.getCurrentItems(this.presentTags);
+      this.currentErrors = this.getCurrentItems(this.presentErrors);
+      this.transitions = this.getCurrentTransitions();
+      this.errors = this.getCurrentErrorTags();
       this.updateCollapsedDiffClass();
     },
     isSelected(isSelected) {
@@ -426,7 +444,48 @@
           marginTop: '0px',
         }
       }
-    }
+    },
+
+    /** Check if tag/error id matches entry id */
+    isIdMatch(a, b) {
+      return a.taskId===b.taskId || a.layerId===b.id;
+    },
+    /** Performs check for id match between entry and present tags/errors
+     * exits once match has been found
+     */
+    matchItems(flickerItems) {
+      var match = false;
+      flickerItems.every(flickerItem => {
+        if (this.isIdMatch(flickerItem, this.item)) {
+          match = true;
+          return false;
+        }
+      });
+      return match;
+    },
+    /** Returns check for id match between entry and present tags/errors */
+    isEntryTagMatch() {
+      return this.matchItems(this.currentTags) || this.matchItems(this.currentErrors);
+    },
+
+    getCurrentItems(items) {
+      if (!items) return [];
+      else return items.filter(item => item.timestamp===this.currentTimestamp);
+    },
+    getCurrentTransitions() {
+      var transitions = [];
+      var ids = [];
+      this.currentTags.forEach(tag => {
+        if (!ids.includes(tag.id) && this.isIdMatch(tag, this.item)) {
+          transitions.push(tag.transition);
+          ids.push(tag.id);
+        }
+      });
+      return transitions;
+    },
+    getCurrentErrorTags() {
+      return this.currentErrors.filter(error => this.isIdMatch(error, this.item));
+    },
   },
   computed: {
     hasDiff() {
@@ -486,7 +545,12 @@
       const offset = levelOffset * (this.depth + this.isLeaf) + 'px';
 
       var display = "";
-      if (this.onlyVisible && !this.item.isVisible) display = 'none';
+      if (!this.item.timestamp
+        && this.flattened
+        && (this.onlyVisible && !this.item.isVisible ||
+            this.flickerTraceView && !this.isEntryTagMatch())) {
+        display = 'none';
+      }
 
       return {
         marginLeft: '-' + offset,
diff --git a/tools/winscope/src/WindowManagerTraceView.vue b/tools/winscope/src/WindowManagerTraceView.vue
index e24426c..d2d709a 100644
--- a/tools/winscope/src/WindowManagerTraceView.vue
+++ b/tools/winscope/src/WindowManagerTraceView.vue
@@ -14,7 +14,13 @@
 -->
 
 <template>
-  <TraceView :store="store" :file="file" :summarizer="summarizer" />
+  <TraceView
+    :store="store"
+    :file="file"
+    :summarizer="summarizer"
+    :presentTags="presentTags"
+    :presentErrors="presentErrors"
+  />
 </template>
 
 <script>
@@ -22,7 +28,7 @@
 
 export default {
   name: "WindowManagerTraceView",
-  props: ["store", "file"],
+  props: ["store", "file", "presentTags", "presentErrors"],
   components: {
     TraceView
   },
diff --git a/tools/winscope/src/components/TagDisplay/Arrow.vue b/tools/winscope/src/components/TagDisplay/Arrow.vue
index 8059de4..0a19705 100644
--- a/tools/winscope/src/components/TagDisplay/Arrow.vue
+++ b/tools/winscope/src/components/TagDisplay/Arrow.vue
@@ -24,7 +24,6 @@
 <style scoped>
 .arrow {
   display: inline-block;
-  position: absolute;
   width: 0;
   height: 0;
   border-left: 6px solid transparent;
diff --git a/tools/winscope/src/components/TagDisplay/TransitionContainer.vue b/tools/winscope/src/components/TagDisplay/TransitionContainer.vue
index 701cbad..0c74c66 100644
--- a/tools/winscope/src/components/TagDisplay/TransitionContainer.vue
+++ b/tools/winscope/src/components/TagDisplay/TransitionContainer.vue
@@ -92,10 +92,12 @@
 }
 
 .arrow-start {
+  position: absolute;
   left: 0%;
 }
 
 .arrow-end {
+  position: absolute;
   right: 0%;
 }