-
Notifications
You must be signed in to change notification settings - Fork 637
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
Handle circular dependency upon dependency removal #506
Handle circular dependency upon dependency removal #506
Conversation
Codecov Report
@@ Coverage Diff @@
## master #506 +/- ##
==========================================
+ Coverage 84.07% 84.09% +0.02%
==========================================
Files 175 175
Lines 5864 5891 +27
Branches 973 981 +8
==========================================
+ Hits 4930 4954 +24
- Misses 822 825 +3
Partials 112 112
Continue to review full report at Codecov.
|
inverseDependencies is not empty. Rather, we recursively check the inverseDependencies to see if it eventually only points to the removed module. If this is the case, then we need to proceed removing dependency instead of returning early.
2e2b4fb
to
ae87904
Compare
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.
Looks great! How much does this impact performance?
'', | ||
new Set(), |
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.
Maybe make these args optional with default values
* Given `inverseDependencies`, tracing back inverse dependencies to | ||
* see if it only leads back to `parentModule`. | ||
*/ | ||
async function canSafelyRemoveFromParentModule<T>( |
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 this function is synchronous? In which case we should remove async
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.
Ahh, nice catch :)
// there isn't circular dependency. Thus, we check if it can be safely remove | ||
// by tracing back the inverseDependencies. | ||
if ( | ||
module.inverseDependencies.size && |
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.
Can we do the module.inverseDependencies.size
check in canSafelyRemoveFromParentModule
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 this may make it clear on what canSafelyRemoveFromParentModule
function intends to do, as way to signify that if module.inverseDependencies.size
is non zero then we need additional check for all of the inverse dependencies.
Thank you so much for this fix. I believe this looks good but do you mind answering @noahsug's questions? I am also curious if there is any measurable performance difference but overall I think it is probably fine – all this data is available in the graph already and no expensive I/O needs to be done for this. If it's less than ~100ms additional time spent for a graph of 10k modules with one change I think we can live with this. |
After running the initial solution with our entrypoint which amounts to 8730 modules, there was obvious performance bug with it (~10s for removing the entrypoint). With more investigation, I implemented a memoized version which helps avoiding unnecessary DFS. With the updated approach, we're looking at 1~2 seconds by removing an entrypoint which has 8730 modules. I also added more tests around various edge cases that I came up with while debugging with our entrypoint. |
76f8c9c
to
fb93ce0
Compare
memoized solution to short circuit any situation when a module does not need further DFS.
fb93ce0
to
d194e41
Compare
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.
Nice work!! My comments are mostly style stuff
for (const dependency of module.dependencies.values()) { | ||
removeDependency(module, dependency.absolutePath, graph, delta); | ||
} | ||
await Promise.all( |
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 don't think there's anything asynchronous going on here, so we can remove the await Promise.all(
. See below for comment.
removeDependency(module, dependency.absolutePath, graph, delta); | ||
} | ||
} | ||
await Promise.all( |
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 don't think there's anything asynchronous going on here, so we can remove the await Promise.all(
. See below for comment.
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.
will do!
return canSafelyRemove; | ||
} | ||
|
||
async function removeDependency<T>( |
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 don't think this is doing anything asynchronous, so we can remove async
.
Since javascript is single threaded, the recursive await Promise.all(
below is actually running synchronously (unless I'm missing something - we're not using jest-worker or anything truly async, right?)
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.
ahhh noo I see what you meant. I think I did remove all the necessary await. updating to address this!
const result = getAllTopLevelInverseDependencies( | ||
inverseDependencies, | ||
graph, | ||
'', |
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.
nit: a comment here like '', // current module name
could be helpful since there isn't a named variable to explain what it does
* this can happen when trying to see if we can safely remove from | ||
* a module that was deleted. This is why we filtered them out with `delta.deleted` | ||
* 2. We have one top module and it is parentModule | ||
* |
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.
nit: remove empty line, and comment length seems inconsistent. I'm not sure what the style guidelines are on that
delta: Delta, | ||
): boolean { | ||
const visited = new Set(); | ||
const result = getAllTopLevelInverseDependencies( |
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.
maybe rename result
to inverseDependencies
? Since result isn't actually the result the function returns
return true; | ||
} | ||
|
||
const filterNotDeletedResult = Array.from(result).filter( |
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.
could rename to something like undeletedInverseDependencies
fa6ce94
to
b836f22
Compare
Just wanted to clarify this is not the usual case for every change, only when removing a module that has 8k+ dependencies of its own, right? Removing a module here and there won't have a big performance impact, is that correct? |
Yes that is correct. Even with 8k module the latency is negligible in my opinion. |
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.
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.
@cpojer is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.
Summary
This is a proposed fix for the issue I submitted #500. For problem statement, please refer to the issue linked to understand the scope of the bug.
Currently Metro does not handle dependency removal correctly in the presence of certain circular dependency. Since we rely on Metro server to produce correct dependency graph in hopes to bundle only the needed modules. This is especially an issue when a dependency is removed from the entry point, however, due to it having circular dependency, the updated dependency graph will leave out all its dependencies in the graph.
I added the following two more test cases to ensure the correctness of the algorithm:
Remove

B
fromE
:removes a dependency with transient cyclic dependency
Remove

B
fromE
:removes a cyclic dependency which is both inverse dependency and direct dependency
Remove

B
fromE
:removes a sub graph that has internal cyclic dependency
Feel free to propose more tests to ensure the correctness of the algorithm. I'm aware that this may introduce more expensive graph updates for certain scenarios, but I believe that ensuring the correctness is far more important for dependency graph updates.
Implementation
Previously, our implementation will stop removing circular dependency due to it having remaining
inverseDependencies
:metro/packages/metro/src/DeltaBundler/traverseDependencies.js
Lines 328 to 330 in 984aab8
This can be illustrated by this example:

When removing
B
fromE
,B
will still have an inverse dependencyA
left. Henceforth, the rest of the graph remain untouched. My proposed solution is to have this async functioncanSafelyRemoveFromParentModule
recursively checking all the inverse dependencies. In this example, we will look up inverse dependencies ofA
all the way to the end until there is no inverse dependency. We can only safely remove this dependency if and only if its end inverse dependency (in this caseA
will haveB
as its end inverse dependency) only has one path and the path is the same as the parent path.Test plan