feat: per-module inter-library dependency filtering (#4572)#14116
feat: per-module inter-library dependency filtering (#4572)#14116robinbb wants to merge 14 commits intoocaml:mainfrom
Conversation
15c3919 to
2b4ab03
Compare
1776c6e to
c6d39e7
Compare
|
Tagging folks who were interested in the now-deprecated PR 14021. This one supersedes that one, with a more elegant solution, and tests that actually pass. It is out of draft mode as of now. |
|
I need help running the so-called revdeps tests, as I have no credentials on this repo and therefore cannot initiate CI runs manually. I also would like to be instructed on how I can compare the results of the revdeps tests on this branch versus those on 'main'. |
Note: reduced package-level cycle detectionThis PR removes the This is an inherent trade-off of the approach: package cycle detection previously piggybacked on compilation-level glob deps as a proxy for declared library dependencies. More precise per-module deps mean that proxy no longer exists for modules that don't actually import their declared libraries. This only affects the case where a module declares a library dependency but never references it in source code. In practice, this is uncommon — but it does mean some package cycles will only be caught at opam-level resolution rather than at build time. |
6e9a47c to
556bd06
Compare
art-w
left a comment
There was a problem hiding this comment.
Very nice! I left some comments regarding the remaining edge-cases where module aliases might cause issues with other features :)
6bc4161 to
2195aaa
Compare
2195aaa to
a2b5350
Compare
a2b5350 to
9550cb5
Compare
|
Note that this PR slows null builds by 30% (see the benchmark job). It is reasonable to expect some overhead from this refining step, but we should strive to reduce it. Getting it within 5% would be ideal. |
d51c71a to
956c365
Compare
Yes, no argument there. I was just giving a reasonable bound on the 'stat' ing part of what must be done. |
Oh! I would have guessed that it first loads its own cached data, and uses that to find the lastModified times of the dune files. It then stats the dune files. Then, if the dune files are older than the cached data, it uses its cached version of the dependency graph. This would require no dune file parsing, no rules logic. It still must stat the files referenced in the deps graphs, however. The 3-step procedure that you mention is what I would expect happens when the dune files are found to be newer than the cached data. |
|
To do something like that dune would need a way to serialize its dependency graph. Turns out that's not so easy. We have a watch mode that makes this issue a lot more bearable for large workspaces, but null build times are still quite important. |
No disagreement from me on that point. However, this PR is now at an impasse. It provides a lot of progress toward the agreed-upon goal of refining and improving the dependency graph - something that is crucial for improving the incremental build situation (and with possible improvements to the build time on clean builds, too). However, this refined build graph is not guaranteed to produce a wall clock time improvement on incremental builds for every build. Unfortunately, this PR produces a null build time regression. If we do not merge this, then we have no progress toward our refined build graph goal, and issue 4572 remains unresolved. If we do merge it, null builds take longer. I propose that this is a "two steps forward, one step back" situation, and that it's worth merging despite the drawback. |
|
Question from a distance (sorry if this has already been mentioned in the discussion): is the root cause of the slowdown understood, at least in principle? And, if so, are there potential approaches available that could be used to mitigate the slowdown in future work? |
|
I am not advocating for blocking this PR until we fix all the performance problems, but it just seems like we're giving up without really trying. If the null build isn't minimized, I think that dune's rules will eventually converge on having both modes and users choosing between the two depending on release vs dev builds. I think that would be suboptimal and a maintenance headache, so I would really prefer to address performance issues early. In the interest of making progress on this PR, I'll propose some actionable steps that we can take to improve performance:
After these two steps, we can consider merging and solving the performance issues separately. I think that's a fair compromise. @art-w could you have another go at reviewing this PR and perhaps pointing out any other performance issues? @nojb do you mind benchmarking this on your internal code base to see if this improves anything (clean or incremental builds)? |
Will do. It will take me a few days to get around to it, though. I will post back here with the results. |
3ee9281 to
1592060
Compare
…caml#4572) Use the already-computed ocamldep output to filter inter-library dependencies on a per-module basis. Each module now only depends on .cmi/.cmx files from libraries it actually imports, rather than glob deps on all files from all dependent libraries. This eliminates unnecessary recompilation when a module in a dependency changes but the current module doesn't reference that library. The implementation: - Add read_immediate_deps_raw_of to ocamldep, returning raw module names without filtering against the stanza's module set - Move Hidden_deps from Includes (library-wide) to per-module in build_cm - Add Lib_index mapping module names to libraries, computed from requires_compile and requires_hidden - In build_cm, use ocamldep output + Lib_index to determine which libraries each module actually needs; fall back to all-library glob deps when filtering is not possible (Melange, virtual library implementations, singleton stanzas, alias/root modules) - For local unwrapped libraries, use per-file deps on specific .cmi/.cmx files rather than directory-wide globs Signed-off-by: Robin Bate Boerop <me@robinbb.com> chore: add changelog entry for per-module dependency filtering (ocaml#4572) Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Remove deps_of_module, cm_kinds_for, and the Module.t option from Lib_index and deps_of_entries. All entries use glob deps (no per-file deps on individual modules within libraries), so the Some m path was dead code. Simplify the types accordingly: Lib_index maps module names to Lib.t list, and deps_of_entries takes Lib.t list directly. Per-module filtering within unwrapped libraries can be revisited in the future once the dependency cone of individual modules is tracked. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Extract the per-module inter-library dependency filtering logic from build_cm into a shared filtered_lib_deps helper. Use it in both build_cm and ocamlc_i so that inferred .mli generation also benefits from per-module filtering. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Use the existing Lib.closure for transitive library dependency closure instead of a hand-rolled close_over function. Lib.closure follows the same requires path and the libraries already passed overlap checks when the compilation context was built. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Remove the main_module_name special case in lib_index construction. For wrapped libraries, entry_modules returns the wrapper module — the same name that main_module_name provided. Since all entries now use glob deps (no per-file Module.t), the two branches were equivalent. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
- Extract all_libs helper to deduplicate requires_compile @ requires_hidden - Expand Module.kind wildcards to explicit variants - Reword fallback comment to not reference removed code - Drop trivial punctuation-only changes to unrelated test files Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Signed-off-by: Robin Bate Boerop <me@robinbb.com>
A module's .mli can reference different libraries than its .ml. Read ocamldep output from both and union them so the filtered set includes all cross-library references. This makes the filtering more precise and prepares for per-module -I flag filtering (ocaml#14186) where the .cmi compilation rule no longer serves as a safety net. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Absorb can_filter into lib_deps_for_module and include m in trans_deps to eliminate duplicated code between build_cm and ocamlc_i. Suggested by art-w. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
read_immediate_deps_raw_of already checks Module.source internally and returns empty when no source file exists. Suggested by art-w. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Signed-off-by: Rudi Grinberg <me@rgrinberg.com>
Extract read_immediate_deps_parsed to parse each .d file once and memoize the result. Both read_immediate_deps_of and read_immediate_deps_raw_of now derive their results from this shared parse, avoiding double parsing on null builds. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
3ee9281 to
3288d55
Compare
|
@rgrinberg Thanks for your suggestions, and thanks for the coding effort, too. I have cherry-picked your commit. I have also addressed the other issue, if I have understood correctly. |
Short answer is 'no', hence @rgrinberg's call to investigate further. |
Together, they have made an impact, but not a big one. The null build time was 6.6 seconds. |
Signed-off-by: Robin Bate Boerop <me@robinbb.com>
|
On PR #14273 I'm trying another experiment to try to reduce the null build time. |
Summary
Use the already-computed
ocamldepoutput to filter inter-librarydependencies on a per-module basis.
Previously, when
libAdepends onlibB, every module oflibAgot aglob dependency on all
.cmifiles inlibB. Now each module onlydepends on
.cmi/.cmxfiles from libraries it actually imports,eliminating unnecessary recompilation.
.mland.mliocamldep output are read for each module, sincethe interface can reference different libraries than the implementation
-openflags are included in the referenced module setmodule M = Mylib): correctlyhandled by transitively closing filtered libraries via
Lib.closure,bounded by the compilation context's library set
Fallback cases (no change from current behavior)
libraries are not in
requires_compile, so filtering falls backback to all libs
falls back
Prerequisite PRs
Fixes #4572