Skip to content

Commit

Permalink
Set framework for visualizing Android dependencies
Browse files Browse the repository at this point in the history
Rough code, mostly copied from wnwen's visualization, as a framework for
future visualization work. It is forecast that this code will be
restructured heavily in the future, hence the lack of more thorough documentation.

npm is used for this project, please read the README on how to run
locally. The dependencies so far are:
- ESLint
- d3 (however, as we continue to identify what is required, I plan to
break this up so we only import the v4 microlibraries we actually need)

The page is currently served the JSON file via a small Python script.
This is easy to use for development, but will hopefully be replaced with
either a user file upload or being served from somewhere else.

Bug: 1093962
Change-Id: I8d35ae2cde64581ff7d22fb06a29cd850e42cc86
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2242171
Commit-Queue: James Long <yjlong@google.com>
Reviewed-by: Samuel Huang <huangs@chromium.org>
Reviewed-by: Henrique Nakashima <hnakashima@chromium.org>
Cr-Commit-Position: refs/heads/master@{#777955}
  • Loading branch information
James Long authored and Commit Bot committed Jun 12, 2020
1 parent 1875930 commit 06c3b58
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 0 deletions.
24 changes: 24 additions & 0 deletions tools/android/dependency_analysis/js/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"env": {
"browser": true,
"es2020": true
},
"extends": [
"google"
],
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
"require-jsdoc": ["warn", {
"require": {
"FunctionDeclaration": true,
"MethodDefinition": true,
"ClassDeclaration": true,
"ArrowFunctionExpression": true,
"FunctionExpression": true
}
}]
}
}
2 changes: 2 additions & 0 deletions tools/android/dependency_analysis/js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
44 changes: 44 additions & 0 deletions tools/android/dependency_analysis/js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Chrome Android Dependency Analysis Visualization
## Development Setup
### Shell variables

This setup assumes Chromium is in a `cr` directory (`~/cr/src/...`). To make setup easier, you can modify and export the following variables:
```
export DEP_ANALYSIS_DIR=~/cr/src/tools/android/dependency_analysis
export DEP_ANALYSIS_JAR=~/cr/src/out/Default/obj/chrome/android/chrome_java__process_prebuilt.desugar.jar
```

### Generate JSON

See `../README.md` for instructions on using `generate_json_dependency_graph.py`, then generate a graph file in this directory (`js/json_graph.txt`) with that exact name:

```
cd $DEP_ANALYSIS_DIR
./generate_json_dependency_graph.py --target $DEP_ANALYSIS_JAR --output js/json_graph.txt
```
**The following instructions assume you are in the `dependency_analysis/js` = `$DEP_ANALYSIS_DIR/js` directory.**

### Install dependencies
You will need to install `npm` if it is not already installed (check with `npm -v`), either [from the site](https://www.npmjs.com/get-npm) or via [nvm](https://github.com/nvm-sh/nvm#about) (Node Version Manager).

To install dependencies:

```
npm install
```

### (TEMP) Run visualization

To run the (highly temporary) Python server to serve the JSON at `localhost:8888/json_graph.txt` :
```
python3 -m http.server 8888
```
The visualization will make requests to this server for the JSON graph on load.

**To view the visualization, open `localhost:8888/index.html`.**

### Miscellaneous
To run [ESLint](https://eslint.org/) on the JS (and fix fixable errors) using [npx](https://www.npmjs.com/package/npx) (bundled with npm):
```
npx eslint --fix *.js
```
23 changes: 23 additions & 0 deletions tools/android/dependency_analysis/js/index.css
Original file line number Diff line number Diff line change
@@ -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. */

.graph-edges line {
stroke: #999;
stroke-opacity: 0.6;
}

.graph-nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}

.graph-labels text {
font-family: sans-serif;
font-size: 12px;
}

circle.dragging {
stroke: #000;
stroke-width: 3;
}
12 changes: 12 additions & 0 deletions tools/android/dependency_analysis/js/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" type="text/css" href="./index.css"></link>
<script type="text/javascript" src="./node_modules/d3/dist/d3.min.js"></script>
</head>
<body>
<svg width="960" height="600"></svg>
<script src="./index.js"></script>
</body>
</html>

118 changes: 118 additions & 0 deletions tools/android/dependency_analysis/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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.

/**
* TODO(yjlong): Compute the "shortest disambiguated name" in Python and include
* it in the JSON so we do not need to do this calculation.
* @param {number} name The full package name to shorten.
* @return {number} The shortened package name.
*/
const shortName = (name) => name.substring(name.lastIndexOf('.') + 1);

/*
* Transforms a graph JSON generated by Python scripts
* (generate_json_dependency_graph.py) into a working format for d3.
* TODO(yjlong): Figure out if we want to typecheck the graph.
*/
function parseNodesAndEdgesFromJson(graph) {
const nodes = graph.nodes.map((node) => ({
...node,
id: node.name,
short_name: shortName(node.name),
}));
const edges = graph.edges.map((edge) => ({
...edge,
id: `${edge.begin}>${edge.end}`,
// The names source/target are needed for d3-force links.
source: edge.begin,
target: edge.end,
}));
return [nodes, edges];
}

function renderJsonGraph(data) {
const [jsonNodes, jsonEdges] = parseNodesAndEdgesFromJson(data.package_graph);

const svg = d3.select('svg');

// TODO(yjlong): SVG should be resizable & these values updated.
const width = +svg.attr('width');
const height = +svg.attr('height');

const simulation = d3.forceSimulation()
.nodes(jsonNodes)
.alphaMin(0.1) // Stop the simulation faster than default (0.001).
.force('chargeForce', d3.forceManyBody().strength(-300))
.force('centerForce', d3.forceCenter(width / 2, height / 2))
.force('links', d3.forceLink(jsonEdges).id((d) => d.id));

const edgeGroup = svg.append('g')
.classed('graph-edges', true)
.attr('stroke-width', 1);
const edges = edgeGroup.selectAll('line')
.data(jsonEdges)
.join((enter) => enter.append('line'));

const dragStarted = (d, i, nodes) => {
d3.event.sourceEvent.stopPropagation();
d3.select(nodes[i]).classed('dragging', false);
d.fx = null;
d.fy = null;
};

const dragged = (d, i, nodes) => {
simulation.alpha(0.3).restart(); // Reheat the simulation on drag.
d3.select(nodes[i]).classed('dragging', true);
// Fix the node's position after it has been dragged.
d.fx = d3.event.x;
d.fy = d3.event.y;
};

const nodeGroup = svg.append('g')
.classed('graph-nodes', true)
.attr('fill', 'red');
const nodes = nodeGroup.selectAll('circle')
.data(jsonNodes)
.join((enter) => enter.append('circle'))
.attr('r', 5)
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged));

const labelGroup = svg.append('g')
.classed('graph-labels', true);
const labels = labelGroup.selectAll('text')
.data(jsonNodes)
.join((enter) => enter.append('text'))
.attr('dx', 12)
.attr('dy', '.35em')
.text((d) => d.short_name);

// The simulation updates position variables in the JSON, it's up to us
// to update the visualization to match on each tick.
const tickActions = () => {
nodes
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);

edges
.attr('x1', (d) => d.source.x)
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y);

labels
.attr('x', (d) => d.x)
.attr('y', (d) => d.y);
};

simulation.on('tick', tickActions);
}

// 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('http://localhost:8888/json_graph.txt')
.then((data) => renderJsonGraph(data));
});
11 changes: 11 additions & 0 deletions tools/android/dependency_analysis/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "dependency-analysis",
"version": "1.0.0",
"dependencies": {
"d3": "^5.16.0"
},
"devDependencies": {
"eslint": "^7.2.0",
"eslint-config-google": "^0.14.0"
}
}

0 comments on commit 06c3b58

Please sign in to comment.