-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add dominance algorithm implementation #326
Merged
yamadapc
merged 2 commits into
main
from
spr/yamadapc/add-dominance-algorithm-implementation
Feb 6, 2025
+318
−0
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"include": ["src/**/*.ts"], | ||
"watermarks": { | ||
"lines": [80, 95], | ||
"functions": [80, 95], | ||
"branches": [80, 95], | ||
"statements": [80, 95] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "@atlaspack/bundler-experimental", | ||
"version": "2.12.0", | ||
"license": "(MIT OR Apache-2.0)", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/atlassian-labs/atlaspack.git" | ||
}, | ||
"main": "lib/index.js", | ||
"source": "src/index.js", | ||
"engines": { | ||
"node": ">= 16.0.0", | ||
"parcel": "^2.12.0" | ||
}, | ||
"dependencies": { | ||
"@atlaspack/core": "2.12.0", | ||
"@atlaspack/diagnostic": "2.12.0", | ||
"@atlaspack/feature-flags": "2.12.0", | ||
"@atlaspack/graph": "3.2.0", | ||
"@atlaspack/logger": "2.12.0", | ||
"@atlaspack/plugin": "2.12.0", | ||
"@atlaspack/rust": "2.12.0", | ||
"@atlaspack/types": "2.12.0", | ||
"@atlaspack/utils": "2.12.0", | ||
"nullthrows": "^1.1.1" | ||
}, | ||
"devDependencies": { | ||
"@atlaspack/fs": "2.12.0" | ||
} | ||
} |
150 changes: 150 additions & 0 deletions
150
...lers/bundler-experimental/src/DominatorBundler/findAssetDominators/simpleFastDominance.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// @flow strict-local | ||
|
||
import {type NodeId, Graph, ALL_EDGE_TYPES} from '@atlaspack/graph'; | ||
|
||
/** | ||
* Implements "A simple, fast dominance algorithm", to find the immediate | ||
* dominators of all nodes in a graph. | ||
* | ||
* Returns a map of node IDs to their immediate dominator's node ID. | ||
* This map is represented by an array where the index is the node ID and the | ||
* value is its dominator. | ||
* | ||
* For example, given a node `3`, `dominators[3]` is the immediate dominator | ||
* of node 3. | ||
* | ||
* - https://www.cs.tufts.edu/comp/150FP/archive/keith-cooper/dom14.pdf | ||
*/ | ||
export function simpleFastDominance<T>(graph: Graph<T, number>): NodeId[] { | ||
const rootNodeId = graph.rootNodeId; | ||
if (rootNodeId == null) { | ||
throw new Error('Graph must have a root node'); | ||
} | ||
|
||
const postOrder = getGraphPostOrder(graph); | ||
const reversedPostOrder = postOrder.slice().reverse(); | ||
const dominators = Array(graph.nodes.length).fill(null); | ||
|
||
const postOrderIndexes = Array(graph.nodes.length).fill(null); | ||
for (let i = 0; i < postOrder.length; i++) { | ||
postOrderIndexes[postOrder[i]] = i; | ||
} | ||
|
||
dominators[rootNodeId] = graph.rootNodeId; | ||
|
||
let changed = true; | ||
|
||
while (changed) { | ||
changed = false; | ||
|
||
for (let node of reversedPostOrder) { | ||
if (node === graph.rootNodeId) continue; | ||
|
||
let newImmediateDominator = null; | ||
graph.forEachNodeIdConnectedTo( | ||
node, | ||
(predecessor) => { | ||
if (newImmediateDominator == null) { | ||
newImmediateDominator = predecessor; | ||
} else { | ||
if (dominators[predecessor] == null) { | ||
return; | ||
} | ||
|
||
newImmediateDominator = intersect( | ||
postOrderIndexes, | ||
dominators, | ||
predecessor, | ||
newImmediateDominator, | ||
); | ||
} | ||
}, | ||
ALL_EDGE_TYPES, | ||
); | ||
|
||
if (dominators[node] !== newImmediateDominator) { | ||
dominators[node] = newImmediateDominator; | ||
changed = true; | ||
} | ||
} | ||
} | ||
|
||
return dominators; | ||
} | ||
|
||
/** | ||
* Return the post-order of the graph. | ||
*/ | ||
export function getGraphPostOrder<T>( | ||
graph: Graph<T, number>, | ||
type: number = 1, | ||
): NodeId[] { | ||
const postOrder = []; | ||
graph.traverse( | ||
{ | ||
exit: (node) => { | ||
postOrder.push(node); | ||
}, | ||
}, | ||
graph.rootNodeId, | ||
type, | ||
); | ||
return postOrder; | ||
} | ||
|
||
/** | ||
* From "A Simple, Fast Dominance Algorithm" | ||
* Keith D. Cooper, Timothy J. Harvey, and Ken Kennedy: | ||
* | ||
* > The intersection routine appears at the bottom of the figure. | ||
* > It implements a “two-finger” algorithm – one can imagine a finger pointing | ||
* > to each dominator set, each finger moving independently as the comparisons | ||
* > dictate. In this case, the comparisons are on postorder numbers; for each | ||
* > intersection, we start the two fingers at the ends of the two sets, and, | ||
* > until the fingers point to the same postorder number, we move the finger | ||
* > pointing to the smaller number back one element. Remember that nodes higher | ||
* > in the dominator tree have higher postorder numbers, which is why intersect | ||
* > moves the finger whose value is less than the other finger’s. When the two | ||
* > fingers point at the same element, intersect returns that element. The set | ||
* > resulting from the intersection begins with the returned element and chains | ||
* > its way up the doms array to the entry node. | ||
* | ||
* `postOrder` is the post-order node list of the graph. | ||
* | ||
* `dominators` is the current immediate dominator state for node in the graph. | ||
* | ||
* This is coupled with the fact node ids are indexes into an array. It is a map | ||
* of NodeId -> NodeId, where the value at index `i` is the immediate dominator | ||
* of the node `i`. | ||
* | ||
* `predecessor` is one predecessor node id of the node we're currently | ||
* computing the immediate dominator for. | ||
* | ||
* `newImmediateDominator` is current best immediate dominator candidate for the | ||
* node we're computing the immediate dominator for. | ||
* | ||
* The algorithm is intersecting the dominator sets of the two predecessors and | ||
* returning dominator node with the highest post-order number by walking up | ||
* the dominator tree until the two sets intersect. | ||
* | ||
* The node with the highest post-order index is the immediate dominator, as | ||
* it is the closest to the node we're computing for. | ||
*/ | ||
export function intersect( | ||
postOrderIndexes: number[], | ||
dominators: (NodeId | null)[], | ||
predecessor: NodeId, | ||
newImmediateDominator: NodeId, | ||
): NodeId { | ||
let n1: number = predecessor; | ||
let n2: number = newImmediateDominator; | ||
while (n1 !== n2) { | ||
while (postOrderIndexes[n1] < postOrderIndexes[n2]) { | ||
n1 = Number(dominators[n1]); | ||
} | ||
while (postOrderIndexes[n2] < postOrderIndexes[n1]) { | ||
n2 = Number(dominators[n2]); | ||
} | ||
} | ||
return n1; | ||
} |
126 changes: 126 additions & 0 deletions
126
...undler-experimental/test/DominatorBundler/findAssetDominators/simpleFastDominance.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// @flow strict-local | ||
|
||
import assert from 'assert'; | ||
import {ContentGraph} from '@atlaspack/graph'; | ||
import {simpleFastDominance} from '../../../src/DominatorBundler/findAssetDominators/simpleFastDominance'; | ||
|
||
const baseGraph = () => { | ||
const inputGraph = new ContentGraph(); | ||
const root = inputGraph.addNodeByContentKey('root', 'root'); | ||
yamadapc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
inputGraph.setRootNodeId(root); | ||
return {inputGraph, root}; | ||
}; | ||
|
||
describe('simpleFastDominance', () => { | ||
it('it works on a linear graph', () => { | ||
// digraph g { | ||
// root -> a -> b -> c -> d | ||
// } | ||
const {inputGraph, root} = baseGraph(); | ||
inputGraph.setRootNodeId(root); | ||
|
||
const a = inputGraph.addNodeByContentKey('a', 'a'); | ||
const b = inputGraph.addNodeByContentKey('b', 'b'); | ||
const c = inputGraph.addNodeByContentKey('c', 'c'); | ||
const d = inputGraph.addNodeByContentKey('d', 'd'); | ||
|
||
inputGraph.addEdge(root, a); | ||
inputGraph.addEdge(a, b); | ||
inputGraph.addEdge(b, c); | ||
inputGraph.addEdge(c, d); | ||
|
||
const dominators = simpleFastDominance(inputGraph); | ||
assert.equal(dominators[root], root); | ||
assert.equal(dominators[a], root); | ||
assert.equal(dominators[b], a); | ||
assert.equal(dominators[c], b); | ||
assert.equal(dominators[d], c); | ||
}); | ||
|
||
it('it works on a tree graph', () => { | ||
// digraph g { | ||
// root -> a; | ||
// root -> b; | ||
// a -> c; | ||
// a -> d; | ||
// b -> e; | ||
// } | ||
const {inputGraph, root} = baseGraph(); | ||
|
||
const a = inputGraph.addNodeByContentKey('a', 'a'); | ||
const b = inputGraph.addNodeByContentKey('b', 'b'); | ||
const c = inputGraph.addNodeByContentKey('c', 'c'); | ||
const d = inputGraph.addNodeByContentKey('d', 'd'); | ||
const e = inputGraph.addNodeByContentKey('e', 'e'); | ||
|
||
inputGraph.addEdge(root, a); | ||
inputGraph.addEdge(root, b); | ||
inputGraph.addEdge(a, c); | ||
inputGraph.addEdge(a, d); | ||
inputGraph.addEdge(b, e); | ||
|
||
const dominators = simpleFastDominance(inputGraph); | ||
assert.equal(dominators[root], root); | ||
assert.equal(dominators[a], root); | ||
assert.equal(dominators[b], root); | ||
assert.equal(dominators[c], a); | ||
assert.equal(dominators[d], a); | ||
assert.equal(dominators[e], b); | ||
}); | ||
|
||
it('it works on simple graph with multiple paths', () => { | ||
// digraph g { | ||
// root -> a; | ||
// a -> b; | ||
// b -> c; | ||
// a -> c; | ||
// } | ||
const {inputGraph, root} = baseGraph(); | ||
|
||
const a = inputGraph.addNodeByContentKey('a', 'a'); | ||
const b = inputGraph.addNodeByContentKey('b', 'b'); | ||
const c = inputGraph.addNodeByContentKey('c', 'c'); | ||
|
||
inputGraph.addEdge(root, a); | ||
inputGraph.addEdge(a, b); | ||
inputGraph.addEdge(b, c); | ||
inputGraph.addEdge(a, c); | ||
|
||
const dominators = simpleFastDominance(inputGraph); | ||
assert.equal(dominators[root], root); | ||
assert.equal(dominators[a], root); | ||
assert.equal(dominators[b], a); | ||
assert.equal(dominators[c], a); | ||
}); | ||
|
||
it('it works on a graph with multiple paths to nodes', () => { | ||
// digraph g { | ||
// root -> a; | ||
// root -> b; | ||
// a -> c; | ||
// a -> d; | ||
// b -> d; | ||
// d -> c; | ||
// } | ||
const {inputGraph, root} = baseGraph(); | ||
|
||
const a = inputGraph.addNodeByContentKey('a', 'a'); | ||
const b = inputGraph.addNodeByContentKey('b', 'b'); | ||
const c = inputGraph.addNodeByContentKey('c', 'c'); | ||
const d = inputGraph.addNodeByContentKey('d', 'd'); | ||
|
||
inputGraph.addEdge(root, a); | ||
inputGraph.addEdge(root, b); | ||
inputGraph.addEdge(a, c); | ||
inputGraph.addEdge(a, d); | ||
inputGraph.addEdge(b, d); | ||
inputGraph.addEdge(d, c); | ||
|
||
const dominators = simpleFastDominance(inputGraph); | ||
assert.equal(dominators[root], root); | ||
assert.equal(dominators[a], root); | ||
assert.equal(dominators[b], root); | ||
assert.equal(dominators[c], root); | ||
assert.equal(dominators[d], root); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason you're using a test folder instead of placing it with the source?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean?
Do you mean using
?
That's just how the repository is setup. I'd rather colocate the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, colocation. Be the change...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could do that for the entire repository separately (?)