perf: faster package-lock tree creation - #20
Conversation
| } | ||
|
|
||
| // prepare a flat map, where keys are path, where dependencies are listed | ||
| const depMap: DepMap = await this.flattenLockfile(lockfile); |
There was a problem hiding this comment.
pretty sure all this async/await in this file is not needed. given no async IO is happening here, the async/await would still block the even-loop until done. So it only adds CPU overhead and complicates the code with various Promise.all()s and stuff
There was a problem hiding this comment.
Yep, that's one of the thing I need your help and expertise - it's not needed now, but this package can end up being used in a deployed service, so we need to be sure we won't block event loop there. I know (from your talk) can push event loop to tick by setImmediate(), but I'd like to discuss the right approach.
|
|
||
| await Promise.all(_.entries(depQueue).map(async ([depKey, dep]) => { | ||
| let i = 0; | ||
| while (i < dep.dependenciesPathsToProcess.length) { |
There was a problem hiding this comment.
it's a while inside a Promise.all - map inside a while - maybe we can simplify it?
b007b66 to
0effcfe
Compare
0effcfe to
a9b6598
Compare
a9b6598 to
82d4476
Compare
| function setImmediatePromise() { | ||
| return new Promise((resolve, reject) => { | ||
| return setImmediate((err) => { | ||
| if (err) { |
There was a problem hiding this comment.
setImmediate can't call callback with error. So, function setImmediatePromise() { return new Promise(resolve => setImmediate(resolve)) }
2950aec to
15afacc
Compare
| depTree.dependencies[dep.name] = await this.buildSubTreeRecursiveFromPackageLock( | ||
| dep.name, ['dependencies'], packageLock, [], dep.dev); | ||
| })); | ||
| // prepare a flat map, where dependency path is a key to dependency object |
There was a problem hiding this comment.
A comment about the if (._isEmpty(...)) above, unrelated to this PR: what will happen if both dependencies and devDepenedencies are empty?
There was a problem hiding this comment.
It would bail out, but it's been fixed in #23
michael-go
left a comment
There was a problem hiding this comment.
This is really great 👏
I added mostly minor comments.
let's also add a test case where the cycle involves the root node (an example real pkg where it kinda happens: https://www.npmjs.com/package/trucolor)
|
|
||
| private createGraphOfDependencies(depMap: DepMap): graphlib.Graph { | ||
| const depGraph = new graphlib.Graph(); | ||
| for (const depName of Object.keys(depMap)) { |
There was a problem hiding this comment.
it's better to name depName as depPath, like below, right? And actually depKey as used in flattenLockfile is even better I think.
| private createGraphOfDependencies(depMap: DepMap): graphlib.Graph { | ||
| const depGraph = new graphlib.Graph(); | ||
| for (const depName of Object.keys(depMap)) { | ||
| depGraph.setNode(depName, depName); |
There was a problem hiding this comment.
the label (second param) is optional & I think you don't use it anywhere
| Once completed for all nodes in a cycle, original cyclic nodes can | ||
| be removed. | ||
| */ | ||
|
|
There was a problem hiding this comment.
remove this blank line? 🙈 sorry
| const depGraph: graphlib.Graph = this.createGraphOfDependencies(depMap); | ||
|
|
||
| // topological sort will be applied and it requires acyclic graphs | ||
| let cycleStarts = {}; // cycle starts are need for top level dependencies |
There was a problem hiding this comment.
please try to improve this comment about cycleStarts. And typing it will help too.
| const newNode = start + uuid(); | ||
| cycleStarts[start] = newNode; | ||
| depMap[newNode] = depMap[start]; | ||
| // update depMap with new node |
There was a problem hiding this comment.
guess the comment is for one line above?
| // 1. Create a uniqe duplicate of entry node (without edges) | ||
| const newNode = start + uuid(); | ||
| cycleStarts[start] = newNode; | ||
| depMap[newNode] = depMap[start]; |
There was a problem hiding this comment.
why here the value is not _.cloneDeep-ed like done in similar operation in walkCycleRec() ?
| depTree.dependencies[dep.name] = dep.dev ? | ||
| this.setDevDepRec(_.cloneDeep(depTrees[depName])) : depTrees[depName]; | ||
| } | ||
| } |
There was a problem hiding this comment.
would add the seImmediatePromise() call in this loop too, as the setDevDepRec might be costly (maybe call it only if dep.dev)
| for (const dep of topLevelDeps) { | ||
| // if any of top level dependencies is a part of cycle | ||
| // it now has a different item in the map | ||
| const depName = cycleStarts[dep.name] ? cycleStarts[dep.name] : dep.name; |
There was a problem hiding this comment.
can be simplified to:
const depName = cycleStarts[dep.name] || dep.name;|
|
||
| if (!depMap[depName]) { | ||
| throw new Error(`Dependency ${depName} was not found in package-lock.json. | ||
| Your package.json and package-lock.json are probably out of sync. |
There was a problem hiding this comment.
when playing with some packages, this error happened to me not because the files were out-of-sync, but because one transitive dep failed to install. So this can happen sometimes not related to top-level deps. FYI
| depNode.requires = Object.keys(dep.requires); | ||
| } | ||
|
|
||
| const depKey: string[] = [...path, depName]; |
There was a problem hiding this comment.
would call this depPath/depPathArray, and depKey would be the join-ed string
There was a problem hiding this comment.
Thanks for the improvements @miiila . approved this PR. Still worry a-little that this won't handle a case where there is a cycle that involves the root package, and it would be great if you can add a test case for this.
| // Since this code doesn't handle any I/O or network, we need to force | ||
| // event loop to tick while being used in server for request processing | ||
| await setImmediatePromise(); | ||
| await this.setImmediatePromise(); |
There was a problem hiding this comment.
it's a minor thing, but would personally prefer the setImmediatePromise to be a static or even global function as it doesn't use any internal state of the the class instance.
| To keep an order of algorithm steps readable, function is defined on-the-fly | ||
| Arrow function is used for calling `this` without .bind(this) in the end | ||
| */ | ||
| const acyclicDuplicationRec = (node, traversed, currentCycle, nodeCopy) => { |
There was a problem hiding this comment.
this is great - and it will be even more readable if this definition moves 2 lines up, outside of the for loop. This way the algorithm can be read more outside-in.
| delete dep.requires; | ||
| depTrees[depKey] = {...dep as PkgTree}; | ||
| const pkgTree: PkgTree = { | ||
| cyclic: dep.cyclic, |
There was a problem hiding this comment.
FYI, I see tests fail as cyclic & hasDevDependencies are always set although they are optional and sometimes not defined on dep.
621ba49 to
e6ad71c
Compare
|
@michael-go I did my best to add a test with self-reference cycle. This was so far the best I was able to create, because I'm not aware about any existing package, which would transitively require itself ( So I created that kind of package by myself and algorithm doesn't break - it identifies cycle one level lower than it really is, but that's the limitation I'm happy to live with. |
|
🎉 This PR is included in version 1.7.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
🚧 DO NOT MERGE 🚧
What this does
Big
package-lock.jsonparsing was slow. This PR introduces better parsing algorithm, which speeds up parsing a lot.Instructions on how to run this locally, background context, what to review, questions…
Proper testing is still needed to be sure that functionality is the same. Tests are passing*, but they can't cover everything. However, it's pretty hard to verify what's right for the output with ~million lines.
* I am aware about one failing, it's a topic for the discussion about the meaning of
depTypeproperty.Before:
After:
30.25s vs 0.39s