Support parsing class graphs in Android dependency visualization
The index page has been forked into class/package versions, which are
identical for now but expected to differ more in the future. Although
there is no way to move between class/package graphs via UI controls as
of yet, it is possible to do so by changing the URL manually (ie., go to
class_index.html instead of package_index.html). The UI control to
generate the current URL also works for class graphs.
Bug: 1093962
Change-Id: I725c9107eb04388ef8228b63dc4ec8c6aa28d9e0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2285327
Commit-Queue: James Long <yjlong@google.com>
Reviewed-by: Henrique Nakashima <hnakashima@chromium.org>
Reviewed-by: Samuel Huang <huangs@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#786400}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 3288fafe1126fc1be1f92e667e9c6c6426cf1961
diff --git a/dependency_analysis/js/chrome_hooks.js b/dependency_analysis/js/chrome_hooks.js
index 68e1de0..b4b845b 100644
--- a/dependency_analysis/js/chrome_hooks.js
+++ b/dependency_analysis/js/chrome_hooks.js
@@ -13,6 +13,16 @@
.replace('chrome.browser.', 'c.b.');
}
+/**
+ * Shortens a class name to be displayed in the svg.
+ * @param {string} name The full class name to shorten.
+ * @return {string} The shortened class name.
+ */
+function shortenClassName(name) {
+ return name.substring(name.lastIndexOf('.') + 1);
+}
+
export {
shortenPackageName,
+ shortenClassName,
};
diff --git a/dependency_analysis/js/class_index.html b/dependency_analysis/js/class_index.html
new file mode 100644
index 0000000..837ffbd
--- /dev/null
+++ b/dependency_analysis/js/class_index.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <link rel="stylesheet" type="text/css" href="./common.css"></link>
+ <script type="text/javascript" src="./node_modules/d3/dist/d3.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+ </head>
+ <body>
+ <div id="class-graph-page"></div>
+ <script type="module" src="./class_index.js"></script>
+ </body>
+</html>
diff --git a/dependency_analysis/js/class_index.js b/dependency_analysis/js/class_index.js
new file mode 100644
index 0000000..c9ca4f5
--- /dev/null
+++ b/dependency_analysis/js/class_index.js
@@ -0,0 +1,23 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {ClassGraphPage} from './vue_components/class_graph_page.js';
+
+// For ease of development, we currently serve all our JSON and other assets
+// through a simple Python server at localhost:8888. This should be changed
+// as we find other ways to serve the assets (user upload or hosted externally).
+const LOCALHOST = 'http://localhost:8888';
+
+// TODO(yjlong): Currently we take JSON served by a Python server running on
+// the side. Replace this with a user upload or pull from some other source.
+document.addEventListener('DOMContentLoaded', () => {
+ d3.json(`${LOCALHOST}/json_graph.txt`).then(data => {
+ new ClassGraphPage({
+ el: '#class-graph-page',
+ propsData: {
+ graphJson: data.class_graph,
+ },
+ });
+ });
+});
diff --git a/dependency_analysis/js/index.css b/dependency_analysis/js/common.css
similarity index 100%
rename from dependency_analysis/js/index.css
rename to dependency_analysis/js/common.css
diff --git a/dependency_analysis/js/index.html b/dependency_analysis/js/package_index.html
similarity index 67%
rename from dependency_analysis/js/index.html
rename to dependency_analysis/js/package_index.html
index ae36c36..cad3283 100644
--- a/dependency_analysis/js/index.html
+++ b/dependency_analysis/js/package_index.html
@@ -1,13 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
- <link rel="stylesheet" type="text/css" href="./index.css"></link>
+ <link rel="stylesheet" type="text/css" href="./common.css"></link>
<script type="text/javascript" src="./node_modules/d3/dist/d3.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="package-graph-page"></div>
- <script type="module" src="./index.js"></script>
+ <script type="module" src="./package_index.js"></script>
</body>
</html>
-
diff --git a/dependency_analysis/js/index.js b/dependency_analysis/js/package_index.js
similarity index 100%
rename from dependency_analysis/js/index.js
rename to dependency_analysis/js/package_index.js
diff --git a/dependency_analysis/js/process_graph_json.js b/dependency_analysis/js/process_graph_json.js
index 1df0e73..a7c0f51 100644
--- a/dependency_analysis/js/process_graph_json.js
+++ b/dependency_analysis/js/process_graph_json.js
@@ -3,7 +3,7 @@
// found in the LICENSE file.
import {Node, GraphModel} from './graph_model.js';
-import {shortenPackageName} from './chrome_hooks.js';
+import {shortenPackageName, shortenClassName} from './chrome_hooks.js';
/**
* A graph read from JSON.
@@ -13,15 +13,23 @@
*/
/**
+ * A function that shortens a full Java name for display in the visualization.
+ * @callback NameShortener
+ * @param {string} name The name to be shortened.
+ * @return {string} The shortened name.
+ */
+
+/**
* Transforms a graph JSON generated by Python scripts
* (generate_json_dependency_graph.py) into a working format for d3.
* @param {!JsonGraph} jsonGraph The JSON graph to parse.
+ * @param {!NameShortener} shortenName The function to shorten full names.
* @return {!GraphModel} The parsed out GraphModel object.
*/
-function parseGraphModelFromJson(jsonGraph) {
+function parseGraphModelFromJson(jsonGraph, shortenName) {
const graph = new GraphModel();
for (const nodeData of jsonGraph.nodes) {
- const node = new Node(nodeData.name, shortenPackageName(nodeData.name));
+ const node = new Node(nodeData.name, shortenName(nodeData.name));
graph.addNodeIfNew(node);
}
for (const edgeData of jsonGraph.edges) {
@@ -33,6 +41,26 @@
return graph;
}
+
+/**
+ * Parses a package JSON graph generated by Python scripts.
+ * @param {!JsonGraph} jsonGraph The JSON package graph to parse.
+ * @return {!GraphModel} The parsed out GraphModel object.
+ */
+function parsePackageGraphModelFromJson(jsonGraph) {
+ return parseGraphModelFromJson(jsonGraph, shortenPackageName);
+}
+
+/**
+ * Parses a class JSON graph generated by Python scripts.
+ * @param {!JsonGraph} jsonGraph The JSON class graph to parse.
+ * @return {!GraphModel} The parsed out GraphModel object.
+ */
+function parseClassGraphModelFromJson(jsonGraph) {
+ return parseGraphModelFromJson(jsonGraph, shortenClassName);
+}
+
export {
- parseGraphModelFromJson,
+ parsePackageGraphModelFromJson,
+ parseClassGraphModelFromJson,
};
diff --git a/dependency_analysis/js/vue_components/class_graph_page.js b/dependency_analysis/js/vue_components/class_graph_page.js
new file mode 100644
index 0000000..13ec369
--- /dev/null
+++ b/dependency_analysis/js/vue_components/class_graph_page.js
@@ -0,0 +1,160 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {CUSTOM_EVENTS} from '../vue_custom_events.js';
+
+import {GraphFilterInput} from './graph_filter_input.js';
+import {GraphFilterItems} from './graph_filter_items.js';
+import {GraphInboundInput} from './graph_inbound_input.js';
+import {GraphOutboundInput} from './graph_outbound_input.js';
+import {GraphVisualization} from './graph_visualization.js';
+import {GraphSelectedNodeDetails} from './graph_selected_node_details.js';
+import {PageUrlGenerator} from './page_url_generator.js';
+
+import {parseClassGraphModelFromJson} from '../process_graph_json.js';
+import {generateFilterFromUrl} from '../url_processor.js';
+import {PageModel} from '../page_model.js';
+import {Node} from '../graph_model.js';
+
+const ClassGraphPage = Vue.component('class-graph-page', {
+ components: {
+ 'graph-filter-input': GraphFilterInput,
+ 'graph-filter-items': GraphFilterItems,
+ 'graph-inbound-input': GraphInboundInput,
+ 'graph-outbound-input': GraphOutboundInput,
+ 'graph-visualization': GraphVisualization,
+ 'graph-selected-node-details': GraphSelectedNodeDetails,
+ 'page-url-generator': PageUrlGenerator,
+ },
+ props: ['graphJson'],
+
+ /**
+ * Various references to objects used across the entire page.
+ * @typedef {Object} PageData
+ * @property {PageModel} pageModel The data store for the page.
+ * @property {number} graphDataUpdateTicker Incremented every time we want to
+ * trigger a visualization update. See graph_visualization.js for further
+ * explanation on this variable.
+ */
+
+ /**
+ * @return {PageData} The objects used throughout the page.
+ */
+ data: function() {
+ const graphModel = parseClassGraphModelFromJson(this.graphJson);
+ const pageModel = new PageModel(graphModel);
+
+ return {
+ pageModel,
+ graphDataUpdateTicker: 0,
+ };
+ },
+ /**
+ * Parses out data from the current URL to initialize the visualization with.
+ */
+ mounted: function() {
+ const includedNodesInUrl = generateFilterFromUrl(document.URL);
+
+ if (includedNodesInUrl.length !== 0) {
+ this.addNodesToFilter(includedNodesInUrl);
+ } else {
+ // TODO(yjlong): This is test data. Remove this when no longer needed.
+ this.addNodesToFilter([
+ 'org.chromium.chrome.browser.tabmodel.AsyncTabParams',
+ 'org.chromium.chrome.browser.ActivityTabProvider',
+ 'org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver',
+ ]);
+ }
+
+ this.setOutboundDepth(1);
+ this.graphDataUpdateTicker++;
+ },
+ methods: {
+ /**
+ * @param {string} nodeName The node to add.
+ */
+ addNodeToFilter: function(nodeName) {
+ this.pageModel.nodeFilterData.addNode(nodeName);
+ this.graphDataUpdateTicker++;
+ },
+ /**
+ * Adds all supplied nodes to the node filter, then increments
+ * `graphDataUpdateTicker` once at the end, even if `nodeNames` is empty.
+ * @param {!Array<string>} nodeNames The nodes to add.
+ */
+ addNodesToFilter: function(nodeNames) {
+ for (const nodeName of nodeNames) {
+ this.pageModel.nodeFilterData.addNode(nodeName);
+ }
+ this.graphDataUpdateTicker++;
+ },
+ /**
+ * @param {string} nodeName The node to remove.
+ */
+ removeNodeFromFilter: function(nodeName) {
+ this.pageModel.nodeFilterData.removeNode(nodeName);
+ this.graphDataUpdateTicker++;
+ },
+ /**
+ * @param {number} depth The new inbound depth.
+ */
+ setInboundDepth: function(depth) {
+ this.pageModel.inboundDepthData.inboundDepth = depth;
+ this.graphDataUpdateTicker++;
+ },
+ /**
+ * @param {number} depth The new outbound depth.
+ */
+ setOutboundDepth: function(depth) {
+ this.pageModel.outboundDepthData.outboundDepth = depth;
+ this.graphDataUpdateTicker++;
+ },
+ /**
+ * @param {?Node} node The selected node. May be `null`, which will reset
+ * the selection to the state with no node.
+ */
+ graphNodeClicked: function(node) {
+ this.pageModel.selectedNodeDetailsData.selectedNode = node;
+ },
+ },
+ template: `
+ <div id="page-container">
+ <div id="page-controls">
+ <graph-filter-input
+ @${CUSTOM_EVENTS.FILTER_SUBMITTED}="this.addNodeToFilter"
+ ></graph-filter-input>
+ <graph-filter-items
+ :node-filter-data="this.pageModel.nodeFilterData"
+ @${CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED}="this.removeNodeFromFilter"
+ ></graph-filter-items>
+ <graph-inbound-input
+ :inbound-depth-data="this.pageModel.inboundDepthData"
+ @${CUSTOM_EVENTS.INBOUND_DEPTH_UPDATED}="this.setInboundDepth"
+ ></graph-inbound-input>
+ <graph-outbound-input
+ :outbound-depth-data="this.pageModel.outboundDepthData"
+ @${CUSTOM_EVENTS.OUTBOUND_DEPTH_UPDATED}="this.setOutboundDepth"
+ ></graph-outbound-input>
+ </div>
+ <div id="graph-and-node-details-container">
+ <graph-visualization
+ :graph-data-update-ticker="this.graphDataUpdateTicker"
+ :page-model="this.pageModel"
+ @${CUSTOM_EVENTS.NODE_CLICKED}="graphNodeClicked"
+ ></graph-visualization>
+ <graph-selected-node-details
+ :selected-node-details-data="this.pageModel.selectedNodeDetailsData"
+ @${CUSTOM_EVENTS.ADD_TO_FILTER_CLICKED}="addNodeToFilter"
+ @${CUSTOM_EVENTS.REMOVE_FROM_FILTER_CLICKED}="removeNodeFromFilter"
+ ></graph-selected-node-details>
+ </div>
+ <page-url-generator
+ :node-filter-data="this.pageModel.nodeFilterData"
+ ></page-url-generator>
+ </div>`,
+});
+
+export {
+ ClassGraphPage,
+};
diff --git a/dependency_analysis/js/vue_components/package_graph_page.js b/dependency_analysis/js/vue_components/package_graph_page.js
index 9baf6db..53f602f 100644
--- a/dependency_analysis/js/vue_components/package_graph_page.js
+++ b/dependency_analysis/js/vue_components/package_graph_page.js
@@ -12,7 +12,7 @@
import {GraphSelectedNodeDetails} from './graph_selected_node_details.js';
import {PageUrlGenerator} from './page_url_generator.js';
-import {parseGraphModelFromJson} from '../process_graph_json.js';
+import {parsePackageGraphModelFromJson} from '../process_graph_json.js';
import {generateFilterFromUrl} from '../url_processor.js';
import {PageModel} from '../page_model.js';
import {Node} from '../graph_model.js';
@@ -42,7 +42,7 @@
* @return {PageData} The objects used throughout the page.
*/
data: function() {
- const graphModel = parseGraphModelFromJson(this.graphJson);
+ const graphModel = parsePackageGraphModelFromJson(this.graphJson);
const pageModel = new PageModel(graphModel);
return {