Implement diff for SurfaceFlinger and WindowManager hierarchy

Test: N/A
Change-Id: I3dea4f29de74a8754cf3164d41c7eb3d76c239a8
diff --git a/tools/winscope/package.json b/tools/winscope/package.json
index 1076abd..57bfd1d 100644
--- a/tools/winscope/package.json
+++ b/tools/winscope/package.json
@@ -11,6 +11,7 @@
   },
   "dependencies": {
     "jszip": "^3.5.0",
+    "lodash.clonedeep": "^4.5.0",
     "vue": "^2.3.3",
     "vue-material": "^1.0.0-beta-11",
     "vuex": "^3.4.0"
@@ -35,4 +36,4 @@
     "webpack": "^2.6.1",
     "webpack-dev-server": "^2.4.5"
   }
-}
+}
\ No newline at end of file
diff --git a/tools/winscope/src/TraceView.vue b/tools/winscope/src/TraceView.vue
index d35c455..13af2ab 100644
--- a/tools/winscope/src/TraceView.vue
+++ b/tools/winscope/src/TraceView.vue
@@ -65,6 +65,7 @@
 import Rects from './Rects.vue'
 
 import { ObjectTransformer } from './transform.js'
+import { DiffGenerator, defaultModifiedCheck } from './utils/diff.js'
 import { format_transform_type, is_simple_transform } from './matrix_utils.js'
 import { DATA_TYPES } from './decode.js'
 import { stableIdCompatibilityFixup } from './utils/utils.js'
@@ -173,6 +174,28 @@
     },
     setData(item) {
       this.tree = item;
+
+      // TODO: Add flag to toggle diffs or not
+      // TODO: Disable if proto are not the latest
+      if (true) {
+        // Required pre-processing to match algo
+        // TODO: Clean this up somehow
+        if (this.file.type == DATA_TYPES.SURFACE_FLINGER) {
+          item.obj.id = -1; // TODO: Make sure this ID can never be used by other objects
+          item.children[0].obj.id = 0;
+        }
+
+        this.tree = new DiffGenerator(item)
+          .compareWith(this.getDataWithOffset(-1))
+          .withUniqueNodeId(node => {
+            return node.stableId;
+          })
+          .withModifiedCheck(defaultModifiedCheck)
+          .generateDiffTree();
+      } else {
+        this.tree = item;
+      }
+
       this.rects = [...item.rects].reverse();
       this.bounds = item.bounds;
 
diff --git a/tools/winscope/src/TreeView.vue b/tools/winscope/src/TreeView.vue
index 5a0402b..76fb656 100644
--- a/tools/winscope/src/TreeView.vue
+++ b/tools/winscope/src/TreeView.vue
@@ -123,6 +123,7 @@
         [DiffType.ADDED]: "+",
         [DiffType.DELETED]: "-",
         [DiffType.MODIFIED]: ".",
+        [DiffType.MOVED]: ".",
       },
     };
   },
@@ -350,7 +351,7 @@
   background: chartreuse;
 }
 
-.tree-view .node:not(.selected).removed {
+.tree-view .node:not(.selected).deleted {
   background: coral;
 }
 
@@ -358,6 +359,11 @@
   background: cyan;
 }
 
+.tree-view .node:not(.selected).addedMove, 
+.tree-view .node:not(.selected).deletedMove {
+  background: slateblue;
+}
+
 .children {
   /* Aligns border with collapse arrows */
   margin-left: 12px;
diff --git a/tools/winscope/src/utils/diff.js b/tools/winscope/src/utils/diff.js
index 4482f03..9208e21 100644
--- a/tools/winscope/src/utils/diff.js
+++ b/tools/winscope/src/utils/diff.js
@@ -1,3 +1,5 @@
+import cloneDeep from 'lodash.clonedeep';
+
 const DiffType = Object.freeze({
     NONE: 'none',
     ADDED: 'added',
@@ -7,4 +9,227 @@
     MODIFIED: 'modified',
 });
 
-export { DiffType };
\ No newline at end of file
+function defaultModifiedCheck(newNode, oldNode) {
+    if (!newNode && !oldNode) {
+        return false;
+    }
+
+    if ((newNode && !oldNode) || (!newNode && oldNode)) {
+        return true;
+    }
+
+    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) {
+                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;
+    }
+}
+
+export { DiffGenerator, DiffType, defaultModifiedCheck }
diff --git a/tools/winscope/yarn.lock b/tools/winscope/yarn.lock
index 9e7e040..924ebac 100644
--- a/tools/winscope/yarn.lock
+++ b/tools/winscope/yarn.lock
@@ -3502,6 +3502,11 @@
   dependencies:
     lodash._createcompounder "^3.0.0"
 
+lodash.clonedeep@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+  integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
 lodash.deburr@^3.0.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-3.2.0.tgz#6da8f54334a366a7cf4c4c76ef8d80aa1b365ed5"