Skip to content

feat: per-module inter-library dependency filtering (#4572)#14116

Open
robinbb wants to merge 14 commits intoocaml:mainfrom
robinbb:robinbb-issue-4572-combined-tests
Open

feat: per-module inter-library dependency filtering (#4572)#14116
robinbb wants to merge 14 commits intoocaml:mainfrom
robinbb:robinbb-issue-4572-combined-tests

Conversation

@robinbb
Copy link
Copy Markdown
Collaborator

@robinbb robinbb commented Apr 10, 2026

Summary

Use the already-computed ocamldep output to filter inter-library
dependencies on a per-module basis.

Previously, when libA depends on libB, every module of libA got a
glob dependency on all .cmi files in libB. Now each module only
depends on .cmi/.cmx files from libraries it actually imports,
eliminating unnecessary recompilation.

  • Both .ml and .mli ocamldep output are read for each module, since
    the interface can reference different libraries than the implementation
  • -open flags are included in the referenced module set
  • Cross-library transparent aliases (module M = Mylib): correctly
    handled by transitively closing filtered libraries via Lib.closure,
    bounded by the compilation context's library set

Fallback cases (no change from current behavior)

  • Virtual library implementations and parameterised modules: parameter
    libraries are not in requires_compile, so filtering falls back
  • Libraries with virtual implementations in requires: filtering falls
    back to all libs
  • Melange mode: filtering is OCaml-only
  • Modules not in the dep graph (e.g., menhir-generated): filtering
    falls back

Prerequisite PRs

Fixes #4572

@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch 3 times, most recently from 15c3919 to 2b4ab03 Compare April 10, 2026 07:20
@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch 6 times, most recently from 1776c6e to c6d39e7 Compare April 10, 2026 21:19
@robinbb robinbb marked this pull request as ready for review April 10, 2026 21:19
@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 10, 2026

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.
@rgrinberg @Leonidas-from-XIV @Khady @nojb @Alizter @art-w

@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 10, 2026

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'.

@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 10, 2026

Note: reduced package-level cycle detection

This PR removes the @package-cycle test case from reporting-of-cycles.t. The underlying cycle still exists (library a.1 declares (libraries b), library b declares (libraries a.2)), but the modules in those libraries are empty — they don't reference any library in their source code. With per-module filtering, the compilation graph no longer has inter-library edges for those modules, so the package-level cycle is no longer detected.

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.

@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch from 6e9a47c to 556bd06 Compare April 11, 2026 21:09
Copy link
Copy Markdown
Collaborator

@art-w art-w left a comment

Choose a reason for hiding this comment

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

Very nice! I left some comments regarding the remaining edge-cases where module aliases might cause issues with other features :)

Comment thread src/dune_rules/lib_file_deps.mli Outdated
Comment thread src/dune_rules/module_compilation.ml Outdated
Comment thread src/dune_rules/module_compilation.ml Outdated
Comment thread src/dune_rules/module_compilation.ml Outdated
Comment thread src/dune_rules/ocamldep.ml Outdated
Comment thread src/dune_rules/compilation_context.ml Outdated
Comment thread src/dune_rules/ocaml_flags.ml Outdated
Comment thread src/dune_rules/compilation_context.ml Outdated
@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch 2 times, most recently from 6bc4161 to 2195aaa Compare April 13, 2026 19:36
@robinbb robinbb marked this pull request as draft April 13, 2026 19:38
@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch from 2195aaa to a2b5350 Compare April 13, 2026 20:06
@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch from a2b5350 to 9550cb5 Compare April 13, 2026 20:38
@rgrinberg
Copy link
Copy Markdown
Member

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.

@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch 2 times, most recently from d51c71a to 956c365 Compare April 13, 2026 22:52
@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 17, 2026

In conclusion, there's a lot more work than just stat'ing. Dune's internal representation of the rules has to be built from scratch from a very high level description and it is hard to make that instant.

Yes, no argument there. I was just giving a reasonable bound on the 'stat' ing part of what must be done.

@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 17, 2026

[...] dune is doing much more than in a null build:

  1. It is parsing every dune file
  2. It is generating the rules for every targets it needs to validate. This is non trivial and requires creating around 10 targets for every source that we've stat'ed.
  3. Then it is confirming that each of those rules is up to date by computing the rule digest against what it saved in the last run

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.

@rgrinberg
Copy link
Copy Markdown
Member

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.

@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 19, 2026

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.

@nojb
Copy link
Copy Markdown
Collaborator

nojb commented Apr 19, 2026

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?

@rgrinberg
Copy link
Copy Markdown
Member

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:

  1. Could you cherry pick my commit in refactor: memoize library closure #14256 into this branch? It should apply cleanly and perhaps reduce the cost of all of these closure.

  2. Could you address the double parsing issue of ocamldep files? We've discussed leaving this for later, but parsing ocamldep files is on the hot path for null builds.

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)?

@nojb
Copy link
Copy Markdown
Collaborator

nojb commented Apr 20, 2026

@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.

@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch from 3ee9281 to 1592060 Compare April 20, 2026 18:39
robinbb and others added 13 commits April 20, 2026 13:06
…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>
@robinbb robinbb force-pushed the robinbb-issue-4572-combined-tests branch from 3ee9281 to 3288d55 Compare April 20, 2026 20:09
@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 20, 2026

@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.

@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 20, 2026

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?

Short answer is 'no', hence @rgrinberg's call to investigate further.

@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 20, 2026

@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.

Together, they have made an impact, but not a big one. The null build time was 6.6 seconds.

Comment thread src/dune_rules/module_compilation.ml Outdated
Signed-off-by: Robin Bate Boerop <me@robinbb.com>
@robinbb
Copy link
Copy Markdown
Collaborator Author

robinbb commented Apr 21, 2026

On PR #14273 I'm trying another experiment to try to reduce the null build time.

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.

Finer dependency analysis between libraries

5 participants