feat: finer dependency analysis between libraries (#4572)#14021
feat: finer dependency analysis between libraries (#4572)#14021robinbb wants to merge 14 commits intoocaml:mainfrom
Conversation
13c8e53 to
618ea7f
Compare
|
Thanks for working on this. I expect users are going to be quite pleased to have this feature. Some preliminary questions:
Why are the include flags are needed if we don't expect the library to be used by the module?
Do we have tests for when an internal module name is shadowing a library?
Is it possible to move these changes to a separate PR and rebase this work on top of that? |
7373af6 to
1892da3
Compare
@rgrinberg I have tried to do so with this PR #14030, depending on what you really meant by "these changes". :-) |
You're very welcome, and thank you for your prompt attention to it! |
1892da3 to
76a7875
Compare
I guess not. I have made #14031 for this. Please have a look. |
74f77e0 to
9752d71
Compare
c2b7f96 to
e22e456
Compare
|
Thanks for working on this patch @robinbb! I tried this in our codebase by adding a new (unused) module to a library which is close to the root of the dependency tree, and doing Do you have any insight as to what may be causing the extra recompilation? |
I think one case where unexpected extra recompilation is triggered is when there a library consisting of a single module. If memory serves there is an optimization that avoids calling |
|
Transparent module aliases hide library references from Here is the regression: This probably also occurs for any module from |
Continuing to investigate this, an triggers the recompilation of the |
e22e456 to
049e00a
Compare
3082bfa to
1fd630d
Compare
…issue ocaml#4572) Add a new function `read_immediate_deps_raw_of` to the ocamldep module that returns raw module names (as Module_name.Set.t) from ocamldep output, without resolving them to Module.t values. This function will be used to determine which external library modules a module actually references, enabling finer-grained dependency tracking where modules are only recompiled when libraries they actually use change. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
37f60f1 to
8a13c95
Compare
| the second run of dune. | ||
|
|
||
| $ dune build @package-cycle | ||
| Error: Dependency cycle between: |
There was a problem hiding this comment.
The removal of this error is strange, because it means we will be able to build packages together in a workspace that otherwise would have cycle errors and cause the (opam and dune) solvers to fail to find a solution.
I think we need to do cycle detection at the level of packages in order to avoid such situations. Before it wasn't needed because the dependencies were coarse enough to imply the sufficient conditions to avoid a cycle.
@rgrinberg any advice on where and how this detection should be done?
Yes, we have a (manually triggered) workflow for this, so provided the branch is rebased on a reasonably recent version of
For smaller package sets this takes about 10 minutes, bigger sets might take several hours I assume. |
8a13c95 to
72009e7
Compare
Change deps_of_entries from Lib.t list to (Lib.t * Module.t option) list. Add deps_of_module (unused). Update all callers to pass (lib, None). No behavioral change — the None path is functionally identical. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
72009e7 to
452bd20
Compare
|
I am chasing down a CI failure in this branch. The failure is good, in the sense that it has caught legitimate regressions introduced by prior versions of this branch - ones that were not caught by the cram test suite. |
Add Lib_index module to lib_file_deps.ml/mli. Not used anywhere yet. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Add lib_index field and accessor to Compilation_context. Wired up with Lib_index.create but not yet used in module_compilation.ml. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Split lib_cm_deps into lib_cm_deps_arg (static Command.Args.t) and lib_cm_deps_dyn (Action_builder, no-op for now). Add can_filter (always false). No behavioral change — lib_cm_deps_arg carries the same Hidden_deps as before, lib_cm_deps_dyn is a no-op. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
Add real can_filter computation (Melange excluded, dep_graph_dir check, module kind check). The value is not used yet — lib_cm_deps_arg still provides static deps for all modules. This tests whether merely computing the can_filter conditions has side effects. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
1f2a0ce to
0e929f1
Compare
Enable can_filter and add full filtering logic in lib_cm_deps_dyn (Lib_index lookup, ocamldep refs, transitive deps, -open extraction). Static lib_cm_deps_arg remains for ALL modules — never removed. Both paths fire: modules get a superset of deps. The filtering optimization does not activate (tests unchanged), but the dynamic machinery runs for real. This tests whether the dyn_deps computation itself causes CI failures. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
4ff661e to
eecf3ac
Compare
|
Very cool work indeed! Is you new PR #14116 superseding this one? Either way, I think it's worth spending more time to improve the test suite, because it's easy to accidentally rely on subtle behaviors that are more permissive than the general case. For example, the Here's a small reproduction which should help debug the issue where a library re-exports one of its dependencies with a module alias. This cram test also shows the subtleties in the setup that could make it pass (when it shouldn't!) First we need a library where we'll perform the changes: Then another library, which exposes an alias to And a binary which depends on Compilation is different when the executable is defined in a single file, so the bug doesn't trigger unless we also add an empty unused file: (!!) The first build succeeds: Now we introduce a "soft" update, such that In that specific case, we are allowed to not recompile But if the Regarding revdeps testing, one issue to keep in mind is that testing a clean build (and not an incremental build) is likely to hide the failures: |
When can_filter is true, remove static Hidden_deps so dyn_deps becomes the sole library dep provider. Modules that don't reference a library are no longer recompiled when that library changes. The can_filter condition excludes Melange, special module kinds, and cases where the dep graph dir doesn't match the obj dir. For sandboxed builds (dune build -p), can_filter is typically false due to the dep_graph_dir mismatch, so static deps are preserved. Test expectations updated from count=2 to count=0. The @package-cycle structural cycle test is updated (empty modules produce no file deps). Also includes -open flag extraction and -as-argument-for/-parameter module name handling to ensure filtering doesn't drop implicit deps. Resolves ocaml#4572 Signed-off-by: Robin Bate Boerop <me@robinbb.com>
12e0764 to
d6ae7e8
Compare
Implements per-module library dependency filtering as proposed in #4572. When
library A depends on library B, Dune currently assumes every module in A
depends on every module in B — causing full recompilation of A whenever any
module in B changes. This PR uses ocamldep's per-module output to declare
cross-library file dependencies only for libraries a module actually
references.
This PR builds on prerequisite PRs:
Hidden_depsfromIncludes.maketo per-module inbuild_cmDepends on #14030.
The changes in this PR (on top of the above):
Lib_index(inlib_file_deps.ml) maps library entry module namesback to their libraries. It is computed once per stanza and stored in
Compilation_context.t.read_immediate_deps_raw_ofreads raw ocamldep output (includingcross-library references that were previously silently dropped), filters
out stanza-internal names, and looks up the remainder in the index to
determine which libraries are actually needed.
generated modules, and unresolvable references fall back to all-library
deps.
Resolves #4572