Skip to content
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

pass1 and pass2 no longer cause a stack overflow for large graphs #228

Merged
merged 12 commits into from
Feb 6, 2018

Conversation

rustedgrail
Copy link
Collaborator

This PR causes pass1 and pass2 to return functions instead of making recursive calls. By returning functions, there is a fixed number of function calls on the stack, regardless of the size of the graph. This is a fix for issue 223.

I ran the tests, and they all pass. Thank you for having comprehensive tests!

@miriammelnick paired with me on the changes.

Copy link

@miriammelnick miriammelnick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM. Left a few suggestions for comments.

// These returned functions are the recursive and base function calls. By
// returning the functions and calling them here, we do not add additional
// function calls to the stack. If recursive case generates additional
// functions calls, they will be prepended to the array.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking the last sentence of this comment:
If the recursive case generates additional functions calls, they will be prepended to the array.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

var min = _.reduce(blockG.outEdges(v), function(min, e) {
pass2(e.w);
return Math.min(min, xs[e.w] - blockG.edge(e));
return _.map(blockG.outEdges(v).reverse(), function(e) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why reverse?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was left over from a previous commit. I removed the reverse.

return _.map(blockG.outEdges(v).reverse(), function(e) {
return function() { return pass2(e.w); };
}).concat([function () {
var min =_.reduce(blockG.outEdges(v), function(acc, e) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a space between = and _

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


// This function handles iterating over the graph without adding functions
// calls to the stack. The argument, helper, returns an array of functions.
// These returned functions are the recursive and base function calls. By

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming "helper" to something more descriptive... maybe something like getNextStepTraversalFuncs . Thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

var traversalFuncs = helper(v);

while(traversalFuncs.length) {
var funcsOrX = traversalFuncs.shift()();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funcsOrX is a non-homogenous array, so I'd recommend adding a comment explicitly stating what types of things it can contain.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}, 0);
return xs[v];
}]);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried adding whitespace in various places but it didn't seem to make it clearer. Probably just leave it as-is.

// function that will set xs[v] with the smallest coordinates.
function pass1(v) {
return _.map(blockG.inEdges(v).reverse(), function(e) {
return function() { return pass1(e.v);};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add space after semicolon before close-curly-brace.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

// pass2 maps over the outEdges of a node and creates a recursive function
// that will be called by iterate. At the end of those functions, there is

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phrasing is awkward.
At the end of those functions, there is another function that will set Finally, it sets xs[v] with the greatest coordinates.

Also, thoughts on "to the greatest coordinates" versus "with the greatest coordinates"? Both here and above, in the comment for pass1.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@lutzroeder lutzroeder assigned lutzroeder and cpettitt and unassigned lutzroeder Jan 25, 2018
@lutzroeder
Copy link
Contributor

I tried this change in Netron and seems to freeze for large graphs. Any ideas why this happens?

Repro steps:

  1. curl https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz | tar xvz
  2. git clone git@github.com:lutzroeder/Netron.git && cd Netron && npm install
  3. npx electron .
  4. Open file downloaded in step 1 --> renders in ~5 seconds
  5. Close the app
  6. Overwrite ./node_modules/dagre/dist with the updated bits
  7. npx electron .
  8. Open the file downloaded in step 1 --> render does not return in more than a minute

@miriammelnick
Copy link

@lutzroeder
How does performance with different sized graphs compare using the old function (before this PR) and the new one?

@lutzroeder
Copy link
Contributor

lutzroeder commented Jan 25, 2018

For the example above time seems to have gone from ~2.5 seconds (old) to infinite (new). Haven't had time to dig in if that's a performance issue or a deadlock and what happens with different files but it seems to be a regression you should investigate.

@rustedgrail
Copy link
Collaborator Author

I can reproduce the issue, but I'm not sure how to debug Netron. My guess is that in the original, visited prevented an infinite of loop. In this PR though, visited is only checked in a parent scope from the recursion and it's not exiting the loop for nodes it's already seen.

@lutzroeder
Copy link
Contributor

lutzroeder commented Jan 30, 2018

Netron is an Electron app so vscode with the Debugger for Chrome extension is one option (run npm install first and set the debugger to Debug Renderer Process). Alternatively you can try to install the python server via pip install netron, run netron inception_v3_2016_08_28_frozen.pb and debug in the browser. Breakpoint on dagre.layout(graph) in view-render.js which is calling into this code.

@rustedgrail
Copy link
Collaborator Author

rustedgrail commented Jan 31, 2018

I saw a lot of repeated 'v' in pass1 when I debugged. Once I moved the visited check to the pass functions, the graph was able to render quickly. Does this commit work you you @lutzroeder?

Copy link
Collaborator

@cpettitt cpettitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to an iterative approach is a nice improvement. However, if I'm not mistaken this could be done more compactly and efficiently by putting the nodes on a stack vs. concatenating lists of traversal functions. Also, I would expect treating the list as a stack vs a queue to be more efficient (Array pop vs shift). A stack would also make this DFS which is consistent with the previous approach. I don't recall if DFS vs BFS was important for these passes.

@rustedgrail
Copy link
Collaborator Author

The way the traversal functions are being added is LIFO. The new functions are added to the front of the array and then we shift off the front of the array.

We could build an ordered array of node ids, appending to it all children node ids. Then reverse the list and iterate over it to calculate the value for xs starting with the lowest children nodes. Is that what you meant? That changes the behavior from setting each xs value as soon as we can to setting all xs values at the end.

@cpettitt
Copy link
Collaborator

cpettitt commented Feb 3, 2018

This is roughly what I was imagining for pass 1. It keeps all of the code for the pass in one place, avoids creating closures, and avoids the more expensive shift call. I have not tested this, so it would need to be verified.

// Pass 1
var xs = {};
var visited = {};
var stack = blockG.nodes();
var elem;
while ((elem = stack.pop())) {
    if (xs.hasOwnProperty(elem)) {
        continue;
    }
    if (visited[elem]) {
        xs[elem] = blockG.inEdges(elem).reduce((max, e) => {
            return Math.max(max, xs[e.v] + blockG.edge(e));
        }, 0);
    } else {
        visited[elem] = true;
        stack.push(elem);
        stack = stack.concat(blockG.predecessors(elem));
    }
}

BTW, this is a bit more complicated than I originally imagined. I didn't look closely enough at the algorithm when I reviewed last time.

@rustedgrail
Copy link
Collaborator Author

I updated the PR with very nearly what you said. I separated pass1 and pass2 because having a reduce inside the while loop causes a warning. Reduce take a function and it references xs and blockG, which are outer scoped variables. Let me know if there is anything else I should update.

"Functions declared within loops referencing an outer scoped variable may lead to confusing semantics."

@cpettitt cpettitt merged commit c6cacf7 into dagrejs:master Feb 6, 2018
@cpettitt
Copy link
Collaborator

cpettitt commented Feb 6, 2018

You're right, that function should not have been in a while loop. Thanks for the enhancement. I'll schedule this for release shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants