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
Move Hidden_deps (library file dependencies) out of Includes.make and into per-module computation in build_cm. This is a pure refactor with no behavioral change — all modules still depend on all libraries in the stanza's requires. This separation is necessary for issue ocaml#4572: Includes carries -I flags (which must be shared across all modules) while Hidden_deps can vary per-module. With this refactor, a future change can use ocamldep output to filter Hidden_deps per-module without affecting -I flags. - Includes.make no longer takes ~opaque or bundles Hidden_deps - New deps_of_entries in lib_file_deps.ml handles opaque logic - Alias and Wrapped_compat modules skip library deps (matching their existing Includes.empty behavior) - deps_with_exts removed (sole caller was the old opaque branch) Signed-off-by: Robin Bate Boerop <me@robinbb.com>
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
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>
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