Refactor WM transformations to use flickerlib object representation

Test: Run winscope and make sure there are no errors and everything seems to still work...
Change-Id: I1aef5c469d4323af502580e03a310d76da739d4d
diff --git a/tools/winscope/package.json b/tools/winscope/package.json
index a917e3e..563add3 100644
--- a/tools/winscope/package.json
+++ b/tools/winscope/package.json
@@ -27,6 +27,7 @@
     "@babel/preset-env": "^7.10.4",
     "@babel/register": "^7.10.5",
     "@jetbrains/kotlin-webpack-plugin": "^3.0.2",
+    "@types/lodash": "^4.14.158",
     "babel-loader": "^8.1.0",
     "compression-webpack-plugin": "^4.0.0",
     "cross-env": "^7.0.2",
@@ -47,6 +48,8 @@
     "protobufjs": "^6.10.0",
     "source-map-loader": "^1.0.1",
     "style-loader": "^1.2.1",
+    "ts-loader": "^8.0.1",
+    "typescript": "^3.9.7",
     "uglifyjs-webpack-plugin": "^2.2.0",
     "vue-loader": "^15.9.3",
     "vue-style-loader": "^4.1.2",
diff --git a/tools/winscope/src/DefaultTreeElement.vue b/tools/winscope/src/DefaultTreeElement.vue
index 5614607..1df8d17 100644
--- a/tools/winscope/src/DefaultTreeElement.vue
+++ b/tools/winscope/src/DefaultTreeElement.vue
@@ -35,6 +35,13 @@
       :title="c.long"
       :class="chipClassForChip(c)"
     >{{c.short}} <!-- No line break on purpose -->
+      <md-tooltip
+        md-delay="300"
+        md-direction="top"
+        style="margin-bottom: -10px"
+      >
+        {{c.long}}
+      </md-tooltip>
     </div>
   </span>
 </template>
diff --git a/tools/winscope/src/Rects.vue b/tools/winscope/src/Rects.vue
index 06d23a4..4b732d7 100644
--- a/tools/winscope/src/Rects.vue
+++ b/tools/winscope/src/Rects.vue
@@ -15,7 +15,7 @@
 <template>
   <div class="bounds" :style="boundsStyle">
     <div
-      class="rect" v-for="r in rects"
+      class="rect" v-for="r in filteredRects"
       :style="rectToStyle(r)"
       @click="onClick(r)"
       v-bind:key="`${r.left}-${r.right}-${r.top}-${r.bottom}-${r.ref.name}`"
@@ -55,6 +55,9 @@
       return this.rectToStyle({top: 0, left: 0, right: this.boundsC.width,
         bottom: this.boundsC.height});
     },
+    filteredRects() {
+      return this.rects.filter((rect) => rect.ref.visible);
+    },
   },
   methods: {
     s(sourceCoordinate) { // translate source into target coordinates
diff --git a/tools/winscope/src/TraceView.vue b/tools/winscope/src/TraceView.vue
index 3dbc941..52016b7 100644
--- a/tools/winscope/src/TraceView.vue
+++ b/tools/winscope/src/TraceView.vue
@@ -209,7 +209,7 @@
     itemSelected(item) {
       this.hierarchySelected = item;
       this.selectedTree = this.getTransformedProperties(item);
-      this.highlight = item.highlight;
+      this.highlight = item.rect;
       this.lastSelectedStableId = item.stableId;
       this.$emit('focus');
     },
diff --git a/tools/winscope/src/decode.js b/tools/winscope/src/decode.js
index eb72e08..42c3830 100644
--- a/tools/winscope/src/decode.js
+++ b/tools/winscope/src/decode.js
@@ -26,7 +26,6 @@
 import jsonProtoDefsLauncher from 'packages/apps/Launcher3/protos/launcher_trace_file.proto';
 import protobuf from 'protobufjs';
 import {transformLayers, transformLayersTrace} from './transform_sf.js';
-import {transform_window_service, transform_window_trace} from './transform_wm.js';
 import {transform_transaction_trace} from './transform_transaction.js';
 import {transform_wl_outputstate, transform_wayland_trace} from './transform_wl.js';
 import {transformProtolog} from './transform_protolog.js';
@@ -211,7 +210,7 @@
     decoderParams: {
       type: FILE_TYPES.WINDOW_MANAGER_TRACE,
       protoType: WmTraceMessage,
-      transform: transform_window_trace,
+      transform: WindowManagerTrace.fromProto,
       timeline: true,
     },
   },
@@ -255,7 +254,7 @@
       type: FILE_TYPES.WINDOW_MANAGER_DUMP,
       mime: 'application/octet-stream',
       protoType: WmDumpMessage,
-      transform: transform_window_service,
+      transform: WindowManagerDump.fromProto,
       timeline: false,
     },
   },
@@ -380,7 +379,7 @@
   const transformed = decodeAndTransformProto(buffer, params, store.displayDefaults);
   let data;
   if (params.timeline) {
-    data = transformed.children;
+    data = transformed.entries ?? transformed.children;
   } else {
     data = [transformed];
   }
diff --git a/tools/winscope/src/dumps/WindowManager.ts b/tools/winscope/src/dumps/WindowManager.ts
index 1203e3e..4a01ef7 100644
--- a/tools/winscope/src/dumps/WindowManager.ts
+++ b/tools/winscope/src/dumps/WindowManager.ts
@@ -17,6 +17,8 @@
 import { FILE_TYPES, DUMP_TYPES } from "@/decode.js";
 import DumpBase from "./DumpBase";
 
+import { WindowManagerTraceEntry } from '@/flickerlib';
+
 export default class WindowManager extends DumpBase {
   wmDumpFile: any;
 
@@ -29,4 +31,8 @@
   get type() {
     return DUMP_TYPES.WINDOW_MANAGER;
   }
+
+  static fromProto(proto): WindowManagerTraceEntry {
+    return WindowManagerTraceEntry.fromProto(proto);
+  }
 }
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/README.md b/tools/winscope/src/flickerlib/README.md
new file mode 100644
index 0000000..79cbbe6
--- /dev/null
+++ b/tools/winscope/src/flickerlib/README.md
@@ -0,0 +1,12 @@
+This directory contains all the code extending the common Flicker library
+to make it fully compatible with Winscope. The common Flicker library is
+written is Kotlin and compiled to JavaScript and then extended by the code in
+this directory.
+
+To use flickerlib in the rest of the Winscope source code use
+`import { ... } from '@/flickerlib'` rather than importing the compiled
+common Flicker library directly.
+
+The flickerlib classes are extended through mixins (functions, getter, and
+setters) that are injected into the original compiled common Flicker library
+classes.
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/WindowManagerTrace.ts b/tools/winscope/src/flickerlib/WindowManagerTrace.ts
new file mode 100644
index 0000000..67f192b
--- /dev/null
+++ b/tools/winscope/src/flickerlib/WindowManagerTrace.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+  WindowManagerTrace,
+} from "./common"
+
+import WindowManagerTraceEntry from "./WindowManagerTraceEntry"
+
+WindowManagerTrace.fromProto = function (proto) {
+  const entries = []
+  for (const entryProto of proto.entry) {
+    const transformedEntry = WindowManagerTraceEntry.fromProto(
+      entryProto.windowManagerService, entryProto.elapsedRealtimeNanos)
+    entries.push(transformedEntry)
+  }
+
+  const source = null;
+  const sourceChecksum = null;
+  return new WindowManagerTrace(entries, source, sourceChecksum)
+}
+
+export default WindowManagerTrace;
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/WindowManagerTraceEntry.ts b/tools/winscope/src/flickerlib/WindowManagerTraceEntry.ts
new file mode 100644
index 0000000..3b76733
--- /dev/null
+++ b/tools/winscope/src/flickerlib/WindowManagerTraceEntry.ts
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { nanosToString, TimeUnits } from "../utils/utils.js"
+
+import { WindowManagerTraceEntry } from "./common"
+
+import { applyMixins } from "./mixin"
+
+import ITreeViewElement from './treeview/IClickableTreeViewElement'
+import IClickableTreeViewElement from './treeview/IClickableTreeViewElement'
+import Chip from './treeview/Chip'
+import WindowContainer from "./windows/WindowContainer"
+
+class WindowManagerTraceEntryMixin implements IClickableTreeViewElement {
+  common: any
+  kind: string
+  name: string
+  shortName: string
+  stableId: string
+  chips: Array<Chip>
+  children: Array<ITreeViewElement>
+  obj: any
+  rawTreeViewObject
+
+  timestamp: number
+  rootWindow
+
+  mixinConstructor(obj) {
+    const name = this.timestamp ? nanosToString(this.timestamp, TimeUnits.MILLI_SECONDS) : ""
+    this.kind = "entry"
+    this.name = name
+    this.shortName = name
+    this.stableId = "entry"
+    this.children = this.rootWindow.children
+    this.chips = []
+    this.obj = obj
+  }
+
+  static fromProto(proto, timestamp) {
+    const rootWindow =
+      WindowContainer.fromProto(proto.rootWindowContainer.windowContainer)
+    const windowManagerTraceEntry =
+      new WindowManagerTraceEntry(rootWindow, timestamp)
+
+    windowManagerTraceEntry.kind = 'service'
+
+    windowManagerTraceEntry.focusedApp = proto.focusedApp
+    windowManagerTraceEntry.focusedDisplayId = proto.focusedDisplayId
+    windowManagerTraceEntry.lastOrientation = proto.lastOrientation
+    windowManagerTraceEntry.policy = proto.policy
+    windowManagerTraceEntry.rotation = proto.rotation
+    windowManagerTraceEntry.displayFrozen = proto.displayFrozen
+    windowManagerTraceEntry.inputMethodWindow = proto.inputMethodWindow
+
+    // Remove anything that is part of the children elements
+    // allows for faster loading of properties and less information cluttering
+    // this applied to anywhere the proto is passed to be saved as .obj
+    const obj = Object.assign({}, proto)
+    obj.rootWindowContainer = {};
+    Object.assign(obj.rootWindowContainer,
+      proto.rootWindowContainer)
+    obj.rootWindowContainer.windowContainer = {};
+    Object.assign(obj.rootWindowContainer.windowContainer,
+      proto.rootWindowContainer.windowContainer)
+    delete obj.rootWindowContainer.windowContainer.children
+    windowManagerTraceEntry.mixinConstructor(obj)
+
+    return windowManagerTraceEntry
+  }
+
+  attachObject(obj) {
+    this.obj = obj
+  }
+
+  asRawTreeViewObject() {
+    // IMPORTANT: We want to always return the same tree view object and not
+    // generate a new one every time this function is called.
+    if (!this.rawTreeViewObject) {
+      const children = this.children.map(child => child.asRawTreeViewObject())
+
+      this.rawTreeViewObject = {
+        kind: this.kind,
+        name: this.name,
+        shortName: this.shortName,
+        stableId: this.stableId,
+        chips: this.chips,
+        obj: this.obj,
+        children,
+        ref: this,
+      }
+    }
+
+    return this.rawTreeViewObject;
+  }
+}
+
+applyMixins(WindowManagerTraceEntry, [WindowManagerTraceEntryMixin])
+
+export default WindowManagerTraceEntry;
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/common.js b/tools/winscope/src/flickerlib/common.js
new file mode 100644
index 0000000..44f70d1
--- /dev/null
+++ b/tools/winscope/src/flickerlib/common.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Imports all the compiled common Flicker library classes and exports them
+// as clean es6 modules rather than having them be commonjs modules
+
+const WindowManagerTrace = require('flicker').com.android.server.wm.flicker
+    .common.traces.windowmanager.WindowManagerTrace;
+const WindowManagerTraceEntry = require('flicker').com.android.server.wm.
+    flicker.common.traces.windowmanager.WindowManagerTraceEntry;
+
+const WindowContainer = require('flicker').com.android.server.wm.flicker.common.
+    traces.windowmanager.windows.WindowContainer;
+const WindowState = require('flicker').com.android.server.wm.flicker.common.
+    traces.windowmanager.windows.WindowState;
+const DisplayContent = require('flicker').com.android.server.wm.flicker.common.
+    traces.windowmanager.windows.DisplayContent;
+const ActivityRecord = require('flicker').com.android.server.wm.flicker.common.
+    traces.windowmanager.windows.ActivityRecord;
+const WindowToken = require('flicker').com.android.server.wm.flicker.common.
+    traces.windowmanager.windows.WindowToken;
+const DisplayArea = require('flicker').com.android.server.wm.flicker.common.
+    traces.windowmanager.windows.DisplayArea;
+const RootDisplayArea = require('flicker').com.android.server.wm.flicker.common.
+    traces.windowmanager.windows.RootDisplayArea;
+const Task = require('flicker').com.android.server.wm.flicker.common.traces.
+    windowmanager.windows.Task;
+
+const Rect = require('flicker').com.android.server.wm.flicker.common.Rect;
+const Bounds = require('flicker').com.android.server.wm.flicker.common.Bounds;
+
+export {
+  WindowManagerTrace,
+  WindowManagerTraceEntry,
+  WindowContainer,
+  WindowState,
+  DisplayContent,
+  ActivityRecord,
+  WindowToken,
+  DisplayArea,
+  RootDisplayArea,
+  Task,
+  Rect,
+  Bounds,
+};
diff --git a/tools/winscope/src/flickerlib/index.js b/tools/winscope/src/flickerlib/index.js
new file mode 100644
index 0000000..9c731b1
--- /dev/null
+++ b/tools/winscope/src/flickerlib/index.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import WindowManagerTraceEntry from './WindowManagerTraceEntry';
+import WindowManagerTrace from './WindowManagerTrace';
+
+/**
+ * Entry point into the flickerlib for Winscope.
+ * Expose everything we want Winscope to have access to here.
+ */
+export {WindowManagerTraceEntry, WindowManagerTrace};
+
diff --git a/tools/winscope/src/flickerlib/mixin.ts b/tools/winscope/src/flickerlib/mixin.ts
new file mode 100644
index 0000000..5e6c836
--- /dev/null
+++ b/tools/winscope/src/flickerlib/mixin.ts
@@ -0,0 +1,23 @@
+/**
+ * Injects all the properties (getters, setters, functions...) of a list of
+ * classes (baseCtors) into a class (derivedCtor).
+ * @param derivedCtor The constructor of the class we want to inject the
+ *                    properties into.
+ * @param baseCtors A list of consturctor of the classes we want to mixin the
+ *                  properties of into the derivedCtor.
+ */
+export function applyMixins(derivedCtor: any, baseCtors: any[]) {
+  baseCtors.forEach(baseCtor => {
+    Object.getOwnPropertyNames(baseCtor).forEach(name => {
+      if (['length', 'name', 'prototype'].includes(name)) {
+        return;
+      }
+
+      Object.defineProperty(derivedCtor, name, Object.getOwnPropertyDescriptor(baseCtor, name))
+    })
+
+    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
+      Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name))
+    })
+  });
+}
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/treeview/Chip.ts b/tools/winscope/src/flickerlib/treeview/Chip.ts
new file mode 100644
index 0000000..30f1c83
--- /dev/null
+++ b/tools/winscope/src/flickerlib/treeview/Chip.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ChipType from "./ChipType"
+
+export default class Chip {
+  short: String
+  long: String
+  type: ChipType
+
+  constructor(short: String, long: String, type: ChipType) {
+    this.short = short
+    this.long = long
+    this.type = type
+  }
+}
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/treeview/ChipType.ts b/tools/winscope/src/flickerlib/treeview/ChipType.ts
new file mode 100644
index 0000000..70c25d7
--- /dev/null
+++ b/tools/winscope/src/flickerlib/treeview/ChipType.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+enum ChipType {
+  DEFAULT = 'default'
+}
+
+export default ChipType
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/treeview/Chips.ts b/tools/winscope/src/flickerlib/treeview/Chips.ts
new file mode 100644
index 0000000..8e1ebd1
--- /dev/null
+++ b/tools/winscope/src/flickerlib/treeview/Chips.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Chip from "./Chip"
+import ChipType from "./ChipType"
+
+export const VISIBLE_CHIP = new Chip("V", "visible", ChipType.DEFAULT)
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/treeview/IClickableTreeViewElement.ts b/tools/winscope/src/flickerlib/treeview/IClickableTreeViewElement.ts
new file mode 100644
index 0000000..7b869cd
--- /dev/null
+++ b/tools/winscope/src/flickerlib/treeview/IClickableTreeViewElement.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ITreeViewElement from "./ITreeViewElement"
+
+export default interface IClickableTreeViewElement extends ITreeViewElement {
+  obj: any
+}
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/treeview/ITreeViewElement.ts b/tools/winscope/src/flickerlib/treeview/ITreeViewElement.ts
new file mode 100644
index 0000000..8e5ab74
--- /dev/null
+++ b/tools/winscope/src/flickerlib/treeview/ITreeViewElement.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Chip from "./Chip"
+import { TreeViewObject } from "./types"
+
+export default interface ITreeViewElement {
+  kind: String
+  name: String
+  shortName: String
+  stableId: Number | String
+  chips: Chip[]
+  children: ITreeViewElement[]
+
+  // This is used for compatibility with the "legacy" Winscope infrastructure
+  // where a class object would cause things to not work properly so this should
+  // return a raw javascript object with the relevant information.
+  // IMPORTANT: The implementation of this function should always return the
+  // same object every time it is called and not generate a new object.
+  asRawTreeViewObject(): TreeViewObject
+}
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/treeview/types.ts b/tools/winscope/src/flickerlib/treeview/types.ts
new file mode 100644
index 0000000..1c5c942
--- /dev/null
+++ b/tools/winscope/src/flickerlib/treeview/types.ts
@@ -0,0 +1,12 @@
+import Chip from './Chip'
+
+export type TreeViewObject = {
+  kind: String
+  name: String
+  shortName: String
+  stableId: String | Number
+  chips: Chip[]
+  obj: any
+  children: TreeViewObject[]
+  ref: any
+}
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/ActivityRecord.ts b/tools/winscope/src/flickerlib/windows/ActivityRecord.ts
new file mode 100644
index 0000000..b3146f6
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/ActivityRecord.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+  ActivityRecord,
+} from "../common"
+
+import { applyMixins } from '../mixin'
+
+import WindowToken from "./WindowToken"
+
+export class ActivityRecordMixin {
+  get kind() {
+    return "DisplayArea"
+  }
+
+  static fromProto(proto) {
+    const windowToken = WindowToken.fromProto(proto.windowToken)
+
+    const activityRecord = new ActivityRecord(windowToken)
+
+    const obj = Object.assign({}, proto)
+    delete proto.windowToken
+    Object.assign(obj, windowToken.obj)
+    activityRecord.attachObject(obj)
+
+    return activityRecord
+  }
+}
+
+applyMixins(ActivityRecord, [ActivityRecordMixin])
+
+export default ActivityRecord
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/DisplayArea.ts b/tools/winscope/src/flickerlib/windows/DisplayArea.ts
new file mode 100644
index 0000000..b497d02
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/DisplayArea.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+  DisplayArea,
+} from "../common"
+
+import { applyMixins } from '../mixin'
+
+import WindowContainer from "./WindowContainer"
+
+export class DisplayAreaMixin {
+  get kind() {
+    return "DisplayArea"
+  }
+
+  static fromProto(proto) {
+    const windowContainer = WindowContainer.fromProto(proto.windowContainer)
+
+    const displayArea = new DisplayArea(windowContainer)
+
+    const obj = Object.assign({}, proto)
+    delete obj.windowContainer
+    Object.assign(obj, windowContainer.obj)
+    displayArea.attachObject(obj)
+
+    return displayArea
+  }
+}
+
+applyMixins(DisplayArea, [DisplayAreaMixin])
+
+export default DisplayArea
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/DisplayContent.ts b/tools/winscope/src/flickerlib/windows/DisplayContent.ts
new file mode 100644
index 0000000..06cd78d
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/DisplayContent.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+  DisplayContent,
+  Bounds
+} from "../common"
+
+import { applyMixins } from '../mixin'
+
+import WindowContainer from "./WindowContainer"
+import DisplayArea from "./DisplayArea"
+
+export class DisplayContentMixin {
+  get kind() {
+    return "DisplayContent"
+  }
+
+  static fromProto(proto) {
+    let rootDisplayArea;
+    if (proto.rootDisplayArea.windowContainer == null) {
+      // For backward compatibility
+      const windowContainer = WindowContainer.fromProto(proto.windowContainer)
+      rootDisplayArea = new DisplayArea(windowContainer)
+    } else {
+      // New protos should always be using this
+      rootDisplayArea = DisplayArea.fromProto(proto.rootDisplayArea)
+    }
+
+    const bounds = new Bounds(
+      proto.displayInfo.logicalWidth || 0,
+      proto.displayInfo.logicalHeight || 0,
+    )
+
+    const displayContent = new DisplayContent(rootDisplayArea, bounds)
+
+    const obj = Object.assign({}, proto)
+    delete obj.windowContainer
+    delete obj.rootDisplayArea
+    Object.assign(obj, rootDisplayArea.obj)
+    displayContent.attachObject(obj)
+
+    return displayContent
+  }
+}
+
+applyMixins(DisplayContent, [DisplayContentMixin])
+
+export default DisplayContent
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/RootDisplayArea.ts b/tools/winscope/src/flickerlib/windows/RootDisplayArea.ts
new file mode 100644
index 0000000..c7acb13
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/RootDisplayArea.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { RootDisplayArea } from "../common"
+import DisplayArea from "./DisplayArea"
+
+RootDisplayArea.fromProto = DisplayArea.fromProto
+
+export default DisplayArea
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/Task.ts b/tools/winscope/src/flickerlib/windows/Task.ts
new file mode 100644
index 0000000..d3b6eba
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/Task.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Task } from "../common"
+
+import { applyMixins } from '../mixin'
+
+import WindowContainer from "./WindowContainer"
+
+export class TaskMixin {
+  get kind() {
+    return "Task"
+  }
+
+  static fromProto(proto) {
+    const windowContainer = WindowContainer.fromProto(proto.windowContainer)
+
+    const task = new Task(windowContainer)
+
+    const obj = Object.assign({}, proto)
+    delete obj.windowContainer
+    Object.assign(obj, windowContainer.obj)
+    task.attachObject(obj)
+
+    return task
+  }
+}
+
+applyMixins(Task, [TaskMixin])
+
+export default Task
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/WindowContainer.ts b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
new file mode 100644
index 0000000..d591739
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/WindowContainer.ts
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { WindowContainer } from "../common"
+
+import { applyMixins } from '../mixin'
+
+import IClickableTreeViewElement from '../treeview/IClickableTreeViewElement'
+import { TreeViewObject } from '../treeview/types'
+
+import DisplayArea from "./DisplayArea"
+import DisplayContent from "./DisplayContent"
+import Task from "./Task"
+import ActivityRecord from "./ActivityRecord"
+import WindowToken from "./WindowToken"
+import WindowState from "./WindowState"
+
+import { CompatibleFeatures } from '@/utils/compatibility.js'
+
+export class WindowContainerMixin implements IClickableTreeViewElement {
+  title: string
+  windowHashCode: number
+  childrenWindows
+  visible: boolean
+  rawTreeViewObject
+
+  _obj: { name: string }
+  _children
+
+  get kind() {
+    return "WindowContainer"
+  }
+
+  get name() {
+    if (this.obj && this.obj.name) {
+      return this.obj.name
+    }
+
+    if (["WindowContainer", "Task"].includes(this.title)) {
+      return null
+    }
+
+    return `${removeRedundancyInName(this.title)}@${this.windowHashCode}`
+  }
+
+  get shortName() {
+    return this.name ? shortenName(this.name) : null
+  }
+
+  get stableId() {
+    return this.windowHashCode
+  }
+
+  get children() {
+    return this._children ?? this.childrenWindows
+  }
+
+  set children(children) {
+    this._children = children
+  }
+
+  get chips() {
+    return []
+  }
+
+  set obj(obj) {
+    this._obj = obj
+  }
+
+  get obj() {
+    return this._obj
+  }
+
+  static fromProto(proto) {
+    const children = proto.children.map(
+      child => transformWindowContainerChildProto(child))
+
+    // A kind of hacky way to check if the proto is from a device which
+    // didn't have the changes required for the diff viewer to work properly
+    // We required that all elements have a stable identifier in order to do the
+    // diff properly. In theory properties diff would still work, but are
+    // currently disabled. If required in the future don't hesitate to make
+    // the required changes.
+    if (!proto.identifier) {
+      CompatibleFeatures.DiffVisualization = false;
+    }
+
+
+    const title = proto.identifier.title
+    const hashCode = proto.identifier.hashCode
+    const visible = proto.visible
+    const windowContainer =
+      new WindowContainer(children, title, hashCode, visible)
+
+    const obj = Object.assign({}, proto)
+    // we remove the children property from the object to avoid it showing the
+    // the properties view of the element as we can always see those elements'
+    // properties by changing the target element in the hierarchy tree view.
+    delete obj.children
+    windowContainer.attachObject(obj)
+
+    return windowContainer
+  }
+
+  attachObject(obj) {
+    this._obj = obj
+  }
+
+  asRawTreeViewObject(): TreeViewObject {
+    // IMPORTANT: We want to always return the same tree view object and not
+    // generate a new one every time this function is called.
+    if (!this.rawTreeViewObject) {
+      const children = this.children.map(child => child.asRawTreeViewObject())
+
+      this.rawTreeViewObject = {
+        kind: this.kind,
+        name: this.name,
+        shortName: this.shortName,
+        stableId: this.stableId,
+        chips: this.chips,
+        obj: this.obj,
+        children,
+        ref: this,
+      };
+    }
+
+    return this.rawTreeViewObject;
+  }
+}
+
+applyMixins(WindowContainer, [WindowContainerMixin])
+
+function transformWindowContainerChildProto(proto) {
+  if (proto.displayArea != null) {
+    return DisplayArea.fromProto(proto.displayArea)
+  }
+
+  if (proto.displayContent != null) {
+    return DisplayContent.fromProto(proto.displayContent)
+  }
+
+  if (proto.task != null) {
+    return Task.fromProto(proto.task)
+  }
+
+  if (proto.activity != null) {
+    return ActivityRecord.fromProto(proto.activity)
+  }
+
+  if (proto.windowToken != null) {
+    return WindowToken.fromProto(proto.windowToken)
+  }
+
+  if (proto.window != null) {
+    return WindowState.fromProto(proto.window)
+  }
+
+  throw new Error("Unhandled WindowContainerChildProto case...")
+}
+
+function removeRedundancyInName(name: string): string {
+  if (!name.includes('/')) {
+    return name
+  }
+
+  const split = name.split('/')
+  const pkg = split[0]
+  var clazz = split.slice(1).join("/")
+
+  if (clazz.startsWith("$pkg.")) {
+    clazz = clazz.slice(pkg.length + 1)
+
+    return "$pkg/$clazz"
+  }
+
+  return name
+}
+
+function shortenName(name: string): string {
+  const classParts = name.split(".")
+
+  if (classParts.length <= 3) {
+    return name
+  }
+
+  const className = classParts.slice(-1)[0] // last element
+
+  return `${classParts[0]}.${classParts[1]}.(...).${className}`
+}
+
+export default WindowContainer
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/WindowState.ts b/tools/winscope/src/flickerlib/windows/WindowState.ts
new file mode 100644
index 0000000..281b3b0
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/WindowState.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { WindowState, Rect } from "../common"
+import { VISIBLE_CHIP } from '../treeview/Chips'
+
+import { applyMixins } from '../mixin'
+
+import WindowContainer from "./WindowContainer"
+
+export class WindowStateMixin {
+  visible: boolean
+
+  get kind() {
+    return "WindowState"
+  }
+
+  static fromProto(proto) {
+    const windowContainer = WindowContainer.fromProto(proto.windowContainer)
+
+    const frame = (proto.windowFrames ?? proto).frame ?? {}
+    const rect = new Rect(frame.left ?? 0, frame.top ?? 0, frame.right ?? 0, frame.bottom ?? 0)
+
+    const windowState =
+      new WindowState(windowContainer, /* childWindows */[], rect)
+
+    const obj = Object.assign({}, proto)
+    Object.assign(obj, windowContainer.obj)
+    delete obj.windowContainer
+    windowState.attachObject(obj)
+
+    return windowState
+  }
+
+  get chips() {
+    return this.visible ? [VISIBLE_CHIP] : []
+  }
+}
+
+applyMixins(WindowState, [WindowStateMixin])
+
+export default WindowState
\ No newline at end of file
diff --git a/tools/winscope/src/flickerlib/windows/WindowToken.ts b/tools/winscope/src/flickerlib/windows/WindowToken.ts
new file mode 100644
index 0000000..07e17cb
--- /dev/null
+++ b/tools/winscope/src/flickerlib/windows/WindowToken.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { WindowToken } from "../common"
+
+import { applyMixins } from '../mixin'
+
+import WindowContainer from "./WindowContainer"
+
+export class WindowContainerMixin {
+  get kind() {
+    return "WindowToken"
+  }
+
+  static fromProto(proto) {
+    const windowContainer = WindowContainer.fromProto(proto.windowContainer)
+
+    const windowToken = new WindowToken(windowContainer)
+
+    const obj = Object.assign({}, proto)
+    Object.assign(obj, windowContainer.obj)
+    delete obj.windowContainer
+    windowToken.attachObject(obj)
+
+    return windowToken
+  }
+}
+
+applyMixins(WindowToken, [WindowContainerMixin])
+
+export default WindowToken
\ No newline at end of file
diff --git a/tools/winscope/src/traces/WindowManager.ts b/tools/winscope/src/traces/WindowManager.ts
index 13c5fc1..bcf5026 100644
--- a/tools/winscope/src/traces/WindowManager.ts
+++ b/tools/winscope/src/traces/WindowManager.ts
@@ -17,6 +17,8 @@
 import { FILE_TYPES, TRACE_TYPES } from '@/decode.js';
 import TraceBase from './TraceBase';
 
+import { WindowManagerTrace } from '@/flickerlib';
+
 export default class WindowManager extends TraceBase {
   wmTraceFile: Object;
 
@@ -30,4 +32,8 @@
   get type() {
     return TRACE_TYPES.WINDOW_MANAGER;
   }
+
+  static fromProto(proto): WindowManagerTrace {
+    return WindowManagerTrace.fromProto(proto);
+  }
 }
diff --git a/tools/winscope/src/transform_wm.js b/tools/winscope/src/transform_wm.js
deleted file mode 100644
index d2bb9bd..0000000
--- a/tools/winscope/src/transform_wm.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright 2017, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { transform, nanos_to_string, get_visible_chip } from './transform.js'
-import { CompatibleFeatures } from './utils/compatibility.js'
-import { getComponentClassName } from './utils/names';
-
-function transform_window(entry) {
-  const chips = [];
-  const renderIdentifier = (id) => shortenComponentName(id.title) + "@" + id.hashCode;
-  const visible = entry.windowContainer.visible;
-  function transform_rect(rect, label) {
-    const r = rect || {};
-    return {
-      left: r.left || 0,
-      right: r.right || 0,
-      top: r.top || 0,
-      bottom: r.bottom || 0,
-      label,
-    }
-  }
-
-  // For backward compatibility
-  if (!entry.windowContainer?.identifier) {
-    CompatibleFeatures.DiffVisualization = false;
-  }
-  const identifier = entry.windowContainer?.identifier ?? entry.identifier;
-  const name = renderIdentifier(identifier);
-  const shortName = getComponentClassName(identifier.title);
-  let rect = transform_rect((entry.windowFrames || entry).frame, name);
-
-  if (visible) {
-    chips.push(get_visible_chip());
-  } else {
-    rect = undefined;
-  }
-
-  return transform({
-    obj: entry,
-    kind: 'window',
-    name,
-    shortName,
-    stableId: entry.windowContainer?.identifier?.hashCode,
-    children: [
-      [entry.childWindows, transform_window],
-      [entry.windowContainer.children.reverse(), transform_window_container_child],
-    ],
-    rect,
-    highlight: rect,
-    chips: chips,
-    visible: visible,
-  });
-}
-
-function transform_activity_record(entry) {
-  return transform({
-    obj: entry,
-    kind: 'activityRecord',
-    name: entry.name,
-    shortName: getComponentClassName(entry.name),
-    stableId: entry.windowToken.windowContainer?.identifier?.hashCode,
-    children: [
-      [entry.windowToken.windows, transform_window],
-      [entry.windowToken.windowContainer.children.reverse(), transform_window_container_child],
-    ],
-  });
-}
-
-function transform_task(entry) {
-  return transform({
-    obj: entry,
-    kind: 'task',
-    name: entry.id || 0,
-    stableId: entry.windowContainer?.identifier?.hashCode,
-    children: [
-      [entry.tasks, transform_task],
-      [entry.activities, transform_activity_record],
-      [entry.windowContainer.children.reverse(), transform_window_container_child],
-    ],
-  });
-}
-
-function transform_stack(entry) {
-  return transform({
-    obj: entry,
-    kind: 'stack',
-    name: entry.id || 0,
-    children: [
-      [entry.tasks, transform_task],
-    ],
-  });
-}
-
-function transform_window_token(entry) {
-  return transform({
-    obj: entry,
-    kind: 'winToken',
-    name: '',
-    stableId: entry.windowContainer?.identifier?.hashCode,
-    children: [
-      [entry.windows, transform_window],
-      [entry.windowContainer.children.reverse(), transform_window_container_child],
-    ],
-  });
-}
-
-function transform_below(entry) {
-  return transform({
-    obj: entry,
-    kind: 'belowAppWindow',
-    name: '',
-    children: [
-      [entry.windows, transform_window],
-    ],
-  });
-}
-
-function transform_above(entry) {
-  return transform({
-    obj: entry,
-    kind: 'aboveAppWindow',
-    name: '',
-    children: [
-      [entry.windows, transform_window],
-    ],
-  });
-}
-
-function transform_ime(entry) {
-  return transform({
-    obj: entry,
-    kind: 'imeWindow',
-    name: '',
-    children: [
-      [entry.windows, transform_window],
-    ],
-  });
-}
-
-function transform_window_container_child(entry) {
-  if (entry.displayArea != null) { return transform_display_area(entry.displayArea) }
-  if (entry.displayContent != null) { return transform_display_content(entry.displayContent) }
-  if (entry.task != null) { return transform_task(entry.task) }
-  if (entry.activity != null) { return transform_activity_record(entry.activity) }
-  if (entry.windowToken != null) { return transform_window_token(entry.windowToken) }
-  if (entry.window != null) { return transform_window(entry.window) }
-
-  // The WindowContainerChild may be unknown
-  return transform({
-    obj: entry,
-    kind: 'WindowContainerChild',
-    name: '',
-    stableId: entry.windowContainer?.identifier?.hashCode,
-    children: [[entry.windowContainer.children.reverse(), transform_window_container_child],]
-  });
-}
-
-
-function transform_display_area(entry) {
-  return transform({
-    obj: entry,
-    kind: 'DisplayArea',
-    name: entry.name,
-    stableId: entry.windowContainer?.identifier?.hashCode,
-    children: [
-      [entry.windowContainer.children.reverse(), transform_window_container_child],
-    ],
-  });
-}
-
-function transform_display_content(entry) {
-  var bounds = {
-    width: entry.displayInfo.logicalWidth || 0,
-    height: entry.displayInfo.logicalHeight || 0,
-  };
-  var children = entry.windowContainer == null
-    ? entry.rootDisplayArea.windowContainer.children.reverse()
-    : entry.windowContainer.children.reverse();
-
-  return transform({
-    obj: entry,
-    kind: 'display',
-    name: entry.id || 0,
-    stableId: entry.windowContainer?.identifier?.hashCode,
-    children: [
-      [entry.aboveAppWindows, transform_above],
-      [entry.imeWindows, transform_ime],
-      [entry.stacks, transform_stack],
-      [entry.tasks, transform_task],
-      [entry.belowAppWindows, transform_below],
-      [children, transform_window_container_child],
-    ],
-    bounds,
-  });
-}
-
-function transform_policy(entry) {
-  return transform({
-    obj: entry,
-    kind: 'policy',
-    name: 'policy',
-    stableId: 'policy',
-    children: [],
-  });
-}
-
-function transform_window_service(entry) {
-  return transform({
-    obj: entry,
-    kind: 'service',
-    name: '',
-    children: [
-      [entry.rootWindowContainer.displays, transform_display_content],
-      [entry.rootWindowContainer.windowContainer.children.reverse(),
-        transform_window_container_child],
-      [[entry.policy], transform_policy],
-    ],
-    timestamp: entry.elapsedRealtimeNanos,
-  });
-}
-
-function transform_entry(entry) {
-  return transform({
-    obj: entry,
-    kind: 'entry',
-    name: nanos_to_string(entry.elapsedRealtimeNanos),
-    children: [
-      [entry.windowManagerService.rootWindowContainer.displays, transform_display_content],
-      [entry.windowManagerService.rootWindowContainer.windowContainer.children.reverse(),
-        transform_window_container_child],
-      [[entry.windowManagerService.policy], transform_policy],
-    ],
-    timestamp: entry.elapsedRealtimeNanos,
-    stableId: 'entry',
-  });
-}
-
-function transform_window_trace(entries) {
-  return transform({
-    obj: entries,
-    kind: 'entries',
-    name: 'entries',
-    children: [
-      [entries.entry, transform_entry],
-    ],
-  });
-}
-
-function shortenComponentName(name) {
-  if (!name.includes('/')) {
-    return name
-  }
-  var split = name.split('/');
-  var pkg = split[0];
-  var clazz = split.slice(1).join('/');
-  if (clazz.startsWith(pkg + '.')) {
-    clazz = clazz.slice(pkg.length + 1);
-    return [pkg, clazz].join('/');
-  }
-  return name;
-}
-
-export { transform_window_service, transform_window_trace };
diff --git a/tools/winscope/src/utils/compatibility.js b/tools/winscope/src/utils/compatibility.js
index bc90da5..5685f37 100644
--- a/tools/winscope/src/utils/compatibility.js
+++ b/tools/winscope/src/utils/compatibility.js
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 const CompatibleFeatures = {
     DiffVisualization: true,
 }
diff --git a/tools/winscope/src/utils/consts.js b/tools/winscope/src/utils/consts.js
index 37f219f..86799d9 100644
--- a/tools/winscope/src/utils/consts.js
+++ b/tools/winscope/src/utils/consts.js
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 /**
  * Should be kept in sync with ENUM is in Google3 under:
  * google3/wireless/android/tools/android_bug_tool/extension/common/actions
diff --git a/tools/winscope/src/utils/diff.js b/tools/winscope/src/utils/diff.js
index 080bb44..7b07606 100644
--- a/tools/winscope/src/utils/diff.js
+++ b/tools/winscope/src/utils/diff.js
@@ -1,236 +1,263 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO (b/162300507): Get rid of cloning
 import cloneDeep from 'lodash.clonedeep';
 
-const DiffType = Object.freeze({
-    NONE: 'none',
-    ADDED: 'added',
-    DELETED: 'deleted',
-    ADDED_MOVE: 'addedMove',
-    DELETED_MOVE: 'deletedMove',
-    MODIFIED: 'modified',
+export const DiffType = Object.freeze({
+  NONE: 'none',
+  ADDED: 'added',
+  DELETED: 'deleted',
+  ADDED_MOVE: 'addedMove',
+  DELETED_MOVE: 'deletedMove',
+  MODIFIED: 'modified',
 });
 
-function defaultModifiedCheck(newNode, oldNode) {
-    if (!newNode && !oldNode) {
-        return false;
-    }
+export function defaultModifiedCheck(newNode, oldNode) {
+  if (!newNode && !oldNode) {
+    return false;
+  }
 
-    if ((newNode && !oldNode) || (!newNode && oldNode)) {
-        return true;
-    }
+  if ((newNode && !oldNode) || (!newNode && oldNode)) {
+    return true;
+  }
 
-    return JSON.stringify(newNode.obj) !== JSON.stringify(oldNode.obj);
+  return JSON.stringify(newNode.obj) !== JSON.stringify(oldNode.obj);
 }
 
 function isPrimitive(test) {
-    return test !== Object(test);
-};
-
-class DiffGenerator {
-    constructor(tree) {
-        this.tree = tree;
-    }
-
-    compareWith(tree) {
-        this.diffWithTree = tree;
-        return this;
-    }
-
-    withUniqueNodeId(getNodeId) {
-        this.getNodeId = node => {
-            const id = getNodeId(node);
-            if (id === null || id === undefined) {
-                console.error("Null node ID for node", node);
-                throw new Error("Node ID can't be null or undefined");
-            }
-            return id;
-        };
-        return this;
-    }
-
-    withModifiedCheck(isModified) {
-        this.isModified = isModified;
-        return this;
-    }
-
-    generateDiffTree() {
-        this.newMapping = this._generateIdToNodeMapping(this.tree);
-        this.oldMapping = this.diffWithTree ? this._generateIdToNodeMapping(this.diffWithTree) : {};
-
-        const diffTrees =
-            this._generateDiffTree(this.tree, this.diffWithTree, [], []);
-
-        let diffTree;
-        if (diffTrees.length > 1) {
-            diffTree = {
-                kind: "",
-                name: "DiffTree",
-                children: diffTrees,
-            };
-        } else {
-            diffTree = diffTrees[0];
-        }
-
-        return Object.freeze(diffTree);
-    }
-
-    _generateIdToNodeMapping(node, acc) {
-        acc = acc || {};
-
-        const nodeId = this.getNodeId(node);
-
-        if (acc[nodeId]) {
-            throw new Error(`Duplicate node id '${nodeId}' detected...`);
-        }
-
-        acc[this.getNodeId(node)] = node;
-
-        if (node.children) {
-            for (const child of node.children) {
-                this._generateIdToNodeMapping(child, acc);
-            }
-        }
-
-        return acc;
-    }
-
-    _objIsEmpty(obj) {
-        return Object.keys(obj).length === 0 && obj.constructor === Object;
-    }
-
-    _cloneNodeWithoutChildren(node) {
-        const clone = {};
-
-        for (const key in node) {
-            if (key !== 'children') {
-                const val = node[key];
-                clone[key] = isPrimitive(val) ? val : cloneDeep(val);
-            }
-        }
-
-        return clone;
-    }
-
-    _generateDiffTree(newTree, oldTree, newTreeSiblings, oldTreeSiblings) {
-        const diffTrees = [];
-
-        // NOTE: A null ID represents a non existent node.
-        const newId = newTree ? this.getNodeId(newTree) : null;
-        const oldId = oldTree ? this.getNodeId(oldTree) : null;
-
-        const newTreeSiblingIds = newTreeSiblings.map(this.getNodeId);
-        const oldTreeSiblingIds = oldTreeSiblings.map(this.getNodeId);
-
-        if (newTree) {
-            // Deep clone newTree omitting children field
-            // Clone is required because trees are frozen objects — we can't
-            // modify the original tree object. Also means there is no side effect.
-            const diffTree = this._cloneNodeWithoutChildren(newTree);
-
-            // Default to no changes
-            diffTree.diff = { type: DiffType.NONE };
-
-            if (newId !== oldId) {
-                // A move, addition, or deletion has occurred
-                let nextOldTree;
-
-                // Check if newTree has been added or moved
-                if (!oldTreeSiblingIds.includes(newId)) {
-                    if (this.oldMapping[newId]) {
-                        // Objected existed in old tree, the counter party
-                        // DELETED_MOVE will be/has been flagged and added to the
-                        // diffTree when visiting it in the oldTree.
-                        diffTree.diff = { type: DiffType.ADDED_MOVE };
-
-                        // TODO: Figure out if oldTree is ever visited now...
-
-                        // Switch out oldTree for new one to compare against
-                        nextOldTree = this.oldMapping[newId];
-                    } else {
-                        diffTree.diff = { type: DiffType.ADDED };
-
-                        // TODO: Figure out if oldTree is ever visited now...
-
-                        // Stop comparing against oldTree
-                        nextOldTree = null;
-                    }
-                }
-
-                // Check if oldTree has been deleted of moved
-                if (oldTree && !newTreeSiblingIds.includes(oldId)) {
-                    const deletedTreeDiff = this._cloneNodeWithoutChildren(oldTree);
-
-                    if (this.newMapping[oldId]) {
-                        deletedTreeDiff.diff = { type: DiffType.DELETED_MOVE };
-
-                        // Stop comparing against oldTree, will be/has been
-                        // visited when object is seen in newTree
-                        nextOldTree = null;
-                    } else {
-                        deletedTreeDiff.diff = { type: DiffType.DELETED };
-
-                        // Visit all children to check if they have been deleted or moved
-                        deletedTreeDiff.children = this._visitChildren(null, oldTree);
-                    }
-
-                    diffTrees.push(deletedTreeDiff);
-                }
-
-                oldTree = nextOldTree;
-            } else {
-                // TODO: Always have modified check and add modified tags on top of others
-                // NOTE: Doesn't check for moved and modified at the same time
-                if (this.isModified && this.isModified(newTree, oldTree)) {
-                    diffTree.diff = { type: DiffType.MODIFIED };
-                }
-            }
-
-            diffTree.children = this._visitChildren(newTree, oldTree);
-            diffTrees.push(diffTree);
-        } else if (oldTree) {
-            if (!newTreeSiblingIds.includes(oldId)) {
-                // Deep clone oldTree omitting children field
-                const diffTree = this._cloneNodeWithoutChildren(oldTree);
-
-                // newTree doesn't exists, oldTree has either been moved or deleted.
-                if (this.newMapping[oldId]) {
-                    diffTree.diff = { type: DiffType.DELETED_MOVE };
-                } else {
-                    diffTree.diff = { type: DiffType.DELETED };
-                }
-
-                diffTree.children = this._visitChildren(null, oldTree);
-                diffTrees.push(diffTree);
-            }
-        } else {
-            throw new Error("Both newTree and oldTree are undefined...");
-        }
-
-        return diffTrees;
-    }
-
-    _visitChildren(newTree, oldTree) {
-        // Recursively traverse all children of new and old tree.
-        const diffChildren = [];
-
-        // TODO: Try replacing this with some sort of zipWith.
-        const numOfChildren = Math.max(
-            newTree?.children.length ?? 0, oldTree?.children.length ?? 0);
-        for (let i = 0; i < numOfChildren; i++) {
-            const newChild = i < newTree?.children.length ?
-                newTree.children[i] : null;
-
-            const oldChild = i < oldTree?.children.length ?
-                oldTree.children[i] : null;
-
-            const childDiffTrees = this._generateDiffTree(
-                newChild, oldChild,
-                newTree?.children ?? [], oldTree?.children ?? []
-            );
-            diffChildren.push(...childDiffTrees);
-        }
-
-        return diffChildren;
-    }
+  return test !== Object(test);
 }
 
-export { DiffGenerator, DiffType, defaultModifiedCheck }
+export class DiffGenerator {
+  constructor(tree) {
+    if (tree.asRawTreeViewObject) {
+      this.tree = tree.asRawTreeViewObject();
+    } else {
+      this.tree = tree;
+    }
+  }
+
+  compareWith(tree) {
+    if (tree?.asRawTreeViewObject) {
+      this.diffWithTree = tree.asRawTreeViewObject();
+    } else {
+      this.diffWithTree = tree;
+    }
+
+    return this;
+  }
+
+  withUniqueNodeId(getNodeId) {
+    this.getNodeId = (node) => {
+      const id = getNodeId(node);
+      if (id === null || id === undefined) {
+        console.error('Null node ID for node', node);
+        throw new Error('Node ID can\'t be null or undefined');
+      }
+      return id;
+    };
+    return this;
+  }
+
+  withModifiedCheck(isModified) {
+    this.isModified = isModified;
+    return this;
+  }
+
+  generateDiffTree() {
+    this.newMapping = this._generateIdToNodeMapping(this.tree);
+    this.oldMapping = this.diffWithTree ?
+      this._generateIdToNodeMapping(this.diffWithTree) : {};
+
+    const diffTrees =
+      this._generateDiffTree(this.tree, this.diffWithTree, [], []);
+
+    let diffTree;
+    if (diffTrees.length > 1) {
+      diffTree = {
+        kind: '',
+        name: 'DiffTree',
+        children: diffTrees,
+        stableId: 'DiffTree',
+      };
+    } else {
+      diffTree = diffTrees[0];
+    }
+
+    return Object.freeze(diffTree);
+  }
+
+  _generateIdToNodeMapping(node, acc) {
+    acc = acc || {};
+
+    const nodeId = this.getNodeId(node);
+
+    if (acc[nodeId]) {
+      throw new Error(`Duplicate node id '${nodeId}' detected...`);
+    }
+
+    acc[this.getNodeId(node)] = node;
+
+    if (node.children) {
+      for (const child of node.children) {
+        this._generateIdToNodeMapping(child, acc);
+      }
+    }
+
+    return acc;
+  }
+
+  _objIsEmpty(obj) {
+    return Object.keys(obj).length === 0 && obj.constructor === Object;
+  }
+
+  _cloneNodeWithoutChildren(node) {
+    const clone = {};
+
+    for (const key in node) {
+      if (key !== 'children') {
+        const val = node[key];
+        clone[key] = isPrimitive(val) ? val : cloneDeep(val);
+      }
+    }
+
+    return clone;
+  }
+
+  _generateDiffTree(newTree, oldTree, newTreeSiblings, oldTreeSiblings) {
+    const diffTrees = [];
+
+    // NOTE: A null ID represents a non existent node.
+    const newId = newTree ? this.getNodeId(newTree) : null;
+    const oldId = oldTree ? this.getNodeId(oldTree) : null;
+
+    const newTreeSiblingIds = newTreeSiblings.map(this.getNodeId);
+    const oldTreeSiblingIds = oldTreeSiblings.map(this.getNodeId);
+
+    if (newTree) {
+      // Deep clone newTree omitting children field
+      // Clone is required because trees are frozen objects — we can't
+      // modify the original tree object. Also means there is no side effect.
+      const diffTree = this._cloneNodeWithoutChildren(newTree);
+
+      // Default to no changes
+      diffTree.diff = {type: DiffType.NONE};
+
+      if (newId !== oldId) {
+        // A move, addition, or deletion has occurred
+        let nextOldTree;
+
+        // Check if newTree has been added or moved
+        if (!oldTreeSiblingIds.includes(newId)) {
+          if (this.oldMapping[newId]) {
+            // Objected existed in old tree, the counter party
+            // DELETED_MOVE will be/has been flagged and added to the
+            // diffTree when visiting it in the oldTree.
+            diffTree.diff = {type: DiffType.ADDED_MOVE};
+
+            // TODO: Figure out if oldTree is ever visited now...
+
+            // Switch out oldTree for new one to compare against
+            nextOldTree = this.oldMapping[newId];
+          } else {
+            diffTree.diff = {type: DiffType.ADDED};
+
+            // TODO: Figure out if oldTree is ever visited now...
+
+            // Stop comparing against oldTree
+            nextOldTree = null;
+          }
+        }
+
+        // Check if oldTree has been deleted of moved
+        if (oldTree && !newTreeSiblingIds.includes(oldId)) {
+          const deletedTreeDiff = this._cloneNodeWithoutChildren(oldTree);
+
+          if (this.newMapping[oldId]) {
+            deletedTreeDiff.diff = {type: DiffType.DELETED_MOVE};
+
+            // Stop comparing against oldTree, will be/has been
+            // visited when object is seen in newTree
+            nextOldTree = null;
+          } else {
+            deletedTreeDiff.diff = {type: DiffType.DELETED};
+
+            // Visit all children to check if they have been deleted or moved
+            deletedTreeDiff.children = this._visitChildren(null, oldTree);
+          }
+
+          diffTrees.push(deletedTreeDiff);
+        }
+
+        oldTree = nextOldTree;
+      } else {
+        // TODO: Always have modified check and add modified tags on top of
+        // others
+        // NOTE: Doesn't check for moved and modified at the same time
+        if (this.isModified && this.isModified(newTree, oldTree)) {
+          diffTree.diff = {type: DiffType.MODIFIED};
+        }
+      }
+
+      diffTree.children = this._visitChildren(newTree, oldTree);
+      diffTrees.push(diffTree);
+    } else if (oldTree) {
+      if (!newTreeSiblingIds.includes(oldId)) {
+        // Deep clone oldTree omitting children field
+        const diffTree = this._cloneNodeWithoutChildren(oldTree);
+
+        // newTree doesn't exists, oldTree has either been moved or deleted.
+        if (this.newMapping[oldId]) {
+          diffTree.diff = {type: DiffType.DELETED_MOVE};
+        } else {
+          diffTree.diff = {type: DiffType.DELETED};
+        }
+
+        diffTree.children = this._visitChildren(null, oldTree);
+        diffTrees.push(diffTree);
+      }
+    } else {
+      throw new Error('Both newTree and oldTree are undefined...');
+    }
+
+    return diffTrees;
+  }
+
+  _visitChildren(newTree, oldTree) {
+    // Recursively traverse all children of new and old tree.
+    const diffChildren = [];
+
+    // TODO: Try replacing this with some sort of zipWith.
+    const numOfChildren = Math.max(
+        newTree?.children.length ?? 0, oldTree?.children.length ?? 0);
+    for (let i = 0; i < numOfChildren; i++) {
+      const newChild = i < newTree?.children.length ?
+        newTree.children[i] : null;
+
+      const oldChild = i < oldTree?.children.length ?
+        oldTree.children[i] : null;
+
+      const childDiffTrees = this._generateDiffTree(
+          newChild, oldChild,
+          newTree?.children ?? [], oldTree?.children ?? [],
+      );
+      diffChildren.push(...childDiffTrees);
+    }
+
+    return diffChildren;
+  }
+}
diff --git a/tools/winscope/src/utils/names.js b/tools/winscope/src/utils/names.js
index abbbbce..a5ae037 100644
--- a/tools/winscope/src/utils/names.js
+++ b/tools/winscope/src/utils/names.js
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 // Returns just the class name and root package information
 function getComponentClassName(componentFullName) {
   const classParts = componentFullName.split('.');
diff --git a/tools/winscope/src/utils/utils.js b/tools/winscope/src/utils/utils.js
index 31dbfa7..f8ec198 100644
--- a/tools/winscope/src/utils/utils.js
+++ b/tools/winscope/src/utils/utils.js
@@ -1,33 +1,87 @@
+/*
+ * Copyright 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 // Find the index of the last element matching the predicate in a sorted array
 function findLastMatchingSorted(array, predicate) {
-    let a = 0;
-    let b = array.length - 1;
-    while (b - a > 1) {
-        const m = Math.floor((a + b) / 2);
-        if (predicate(array, m)) {
-            a = m;
-        } else {
-            b = m - 1;
-        }
+  let a = 0;
+  let b = array.length - 1;
+  while (b - a > 1) {
+    const m = Math.floor((a + b) / 2);
+    if (predicate(array, m)) {
+      a = m;
+    } else {
+      b = m - 1;
     }
+  }
 
-    return predicate(array, b) ? b : a;
+  return predicate(array, b) ? b : a;
 }
 
 // Make sure stableId is unique (makes old versions of proto compatible)
 function stableIdCompatibilityFixup(item) {
-    // For backwards compatibility
-    // (the only item that doesn't have a unique stable ID in the tree)
-    if (item.stableId === 'winToken|-|') {
-        return item.stableId + item.children[0].stableId;
-    }
+  // For backwards compatibility
+  // (the only item that doesn't have a unique stable ID in the tree)
+  if (item.stableId === 'winToken|-|') {
+    return item.stableId + item.children[0].stableId;
+  }
 
-    return item.stableId;
+  return item.stableId;
 }
 
 const DIRECTION = Object.freeze({
-    BACKWARD: -1,
-    FORWARD: 1,
+  BACKWARD: -1,
+  FORWARD: 1,
 });
 
-export { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup }
\ No newline at end of file
+const TimeUnits = Object.freeze({
+  NANO_SECONDS: 'ns',
+  MILLI_SECONDS: 'ms',
+  SECONDS: 's',
+  MINUTES: 'm',
+  HOURS: 'h',
+  DAYS: 'd',
+});
+
+function nanosToString(elapsedRealtimeNanos, precision) {
+  const units = [
+    [1000000, TimeUnits.NANO_SECONDS],
+    [1000, TimeUnits.MILLI_SECONDS],
+    [60, TimeUnits.SECONDS],
+    [60, TimeUnits.MINUTES],
+    [24, TimeUnits.HOURS],
+    [Infinity, TimeUnits.DAYS],
+  ];
+
+  const parts = [];
+
+  let precisionThresholdReached = false;
+  units.some(([div, timeUnit], i) => {
+    const part = (elapsedRealtimeNanos % div).toFixed()
+    if (timeUnit === precision) {
+      precisionThresholdReached = true;
+    }
+    if (precisionThresholdReached) {
+      parts.push(part + timeUnit);
+    }
+    elapsedRealtimeNanos = Math.floor(elapsedRealtimeNanos / div);
+
+    return elapsedRealtimeNanos == 0;
+  });
+
+  return parts.reverse().join('');
+}
+
+export { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup, nanosToString, TimeUnits }
\ No newline at end of file
diff --git a/tools/winscope/webpack.config.common.js b/tools/winscope/webpack.config.common.js
index f2fc4fa..f2faddb 100644
--- a/tools/winscope/webpack.config.common.js
+++ b/tools/winscope/webpack.config.common.js
@@ -44,6 +44,9 @@
     polyfill: '@babel/polyfill',
     main: './src/main.js',
   },
+  externals: {
+    _: 'lodash',
+  },
   resolve: {
     extensions: ['.tsx', '.ts', '.js', '.vue'],
     alias: {
diff --git a/tools/winscope/yarn.lock b/tools/winscope/yarn.lock
index 679450c..238e2e1 100644
--- a/tools/winscope/yarn.lock
+++ b/tools/winscope/yarn.lock
@@ -995,6 +995,11 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
 
+"@types/lodash@^4.14.158":
+  version "4.14.158"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.158.tgz#b38ea8b6fe799acd076d7a8d7ab71c26ef77f785"
+  integrity sha512-InCEXJNTv/59yO4VSfuvNrZHt7eeNtWQEgnieIA+mIC+MOWM9arOWG2eQ8Vhk6NbOre6/BidiXhkZYeDY9U35w==
+
 "@types/long@^4.0.1":
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@@ -2791,6 +2796,15 @@
     memory-fs "^0.5.0"
     tapable "^1.0.0"
 
+enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d"
+  integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ==
+  dependencies:
+    graceful-fs "^4.1.2"
+    memory-fs "^0.5.0"
+    tapable "^1.0.0"
+
 enquirer@^2.3.5, enquirer@^2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
@@ -7101,7 +7115,18 @@
     micromatch "^4.0.0"
     semver "^6.0.0"
 
-tslib@^1.9.0, tslib@^1.9.3:
+ts-loader@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.1.tgz#9670dcbce2a8c8506d01a37fee042350d02c8c21"
+  integrity sha512-I9Nmly0ufJoZRMuAT9d5ijsC2B7oSPvUnOJt/GhgoATlPGYfa17VicDKPcqwUCrHpOkCxr/ybLYwbnS4cOxmvQ==
+  dependencies:
+    chalk "^2.3.0"
+    enhanced-resolve "^4.0.0"
+    loader-utils "^1.0.2"
+    micromatch "^4.0.0"
+    semver "^6.0.0"
+
+tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
   integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==