Skip to content

Add counts of deps and rev-deps in the list of packages - #259

Open
ferminr wants to merge 4 commits into
ocaml-opam:masterfrom
ferminr:master
Open

Add counts of deps and rev-deps in the list of packages#259
ferminr wants to merge 4 commits into
ocaml-opam:masterfrom
ferminr:master

Conversation

@ferminr

@ferminr ferminr commented Aug 7, 2026

Copy link
Copy Markdown

Adds columns Dependencies and Reverse dependencies (counts) to the list of packages.

Adds column Dependency cone (count), an over-approximation of counting the output of opam list --rec --required-by, because it includes packages of every platform (os, arch, ...).

Note: the dependency cone is calculated by first doing a topological sort of dependencies with OcamlGraph. This wouldn't produce an accurate cone if the graph had cycles. As far as I can tell, this shouldn't happen, because we skip dependencies marked as post (Comment about post in the opam manual:

This flag can be used to break dependency cycles)

@kit-ty-kate

Copy link
Copy Markdown
Member

Pushed to staging. Let's see how this looks.
Just as a drive-by comment for now, i'm wondering if it would make sense to add a button to sort by number of rev-deps (number of deps makes less sense i guess)

@kit-ty-kate kit-ty-kate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not immediately convinced by these two fields. While the number of direct reverse dependencies can be useful (to gauge usage), the number of direct dependencies (as opposed to the number of overall dependencies, as-in recursive) is of debatable utility i feel.

While more costly to generate i feel like it would be more useful to give the number of overall dependencies for each packages. This way it would be easy to see at a glance how big the dependency chain/attack surface of a package is. As-is in your PR for example, packages such as async show only 11 dependencies whereas in reality it requires 114 packages to be installed

Comment thread content/universe.xhtml Outdated
Comment on lines +23 to +24
<th>Dependencies</th>
<th>Reverse dependencies</th>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The full size names (extending the size of the column) feel a bit cluttery on the screen, maybe adding shorter names for the columns would help, like Deps and RDeps

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Renamed to Rev-deps and Dep-cone (but I can change to, say, RDeps and DepCone)

@ferminr
ferminr requested a review from kit-ty-kate August 10, 2026 15:10
Comment thread src/o2wUniverse.ml
Comment on lines +142 to +157
(* A graph where nodes are packages and there is an edge a->b if package a depends on package b. *)
module PkgGraph = struct
type t = package_set package_map
module V = OpamPackage
let iter_vertex f deps = OpamPackage.Map.iter (fun pkg _ -> f pkg) deps
let iter_succ f deps pkg =
match OpamPackage.Map.find_opt pkg deps with
| None -> ()
| Some pkgs -> OpamPackage.Set.iter f pkgs
end

module PkgTopo = Graph.Topological.Make (PkgGraph)

let dependency_cone_sizes depends =
(* packages in reverse topological order *)
let pkgs = PkgTopo.fold (fun pkg acc -> pkg :: acc) depends [] in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do you need a graph here? Isn't getting the cardinal of max disjunction sufficient?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not sure what you mean by "max disjuction". If we computed the dependency cone for each package in the switch independently (using, for ex, OpamSwitchState.dependencies) there'd be too much recomputation.

Argument depends here is already the representation of a directed graph (a map from pkg to list of pkgs, ie, an adjacency list representation). Using this, we can reuse OcamlGraph for traversals to avoid recomputation. Or we can write a custom traversal (but it's still a graph traversal)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ah, yeah i was originally thinking of a brute force approach + hashtable to avoid recomputation, but it is 25x slower if you take the whole universe. The topological graph approach is clever, neat approximation.

Comment thread src/o2wUniverse.ml Outdated
Comment on lines +129 to +136
let cone_depends st =
OpamPackage.Map.fold (fun pkg opam ->
let { build; post; test; doc; dev_setup; depopts; _} : OpamListCommand.dependency_toggles =
OpamListCommand.default_dependency_toggles in
let deps =
OpamFormula.packages st.packages @@
OpamPackageVar.all_depends ~build ~post ~test
~doc ~dev_setup ~depopts ~filter_default:true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something like this would be closer to reality. Counting post dependencies will break the graph half the time anyway and inflates the number of dependencies of the ocaml package artificially. Same for disjuctions

Suggested change
let cone_depends st =
OpamPackage.Map.fold (fun pkg opam ->
let { build; post; test; doc; dev_setup; depopts; _} : OpamListCommand.dependency_toggles =
OpamListCommand.default_dependency_toggles in
let deps =
OpamFormula.packages st.packages @@
OpamPackageVar.all_depends ~build ~post ~test
~doc ~dev_setup ~depopts ~filter_default:true
let rec remove_disjunction = function
| (OpamFormula.Empty as x) | (Atom _ as x) -> x
| Block x -> remove_disjunction x
| And (a, b) -> And (remove_disjunction a, remove_disjunction b)
| Or (a, _) -> remove_disjunction a
let cone_depends st =
OpamPackage.Map.fold (fun pkg opam ->
let deps =
OpamFormula.packages st.OpamStateTypes.packages @@
remove_disjunction @@
OpamPackageVar.all_depends ~build:true ~post:false ~test:false
~doc:false ~dev_setup:false ~depopts:false ~filter_default:true

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ah, good idea. Done in commit c511d73

Comment thread src/o2wUniverse.ml
Comment on lines +142 to +157
(* A graph where nodes are packages and there is an edge a->b if package a depends on package b. *)
module PkgGraph = struct
type t = package_set package_map
module V = OpamPackage
let iter_vertex f deps = OpamPackage.Map.iter (fun pkg _ -> f pkg) deps
let iter_succ f deps pkg =
match OpamPackage.Map.find_opt pkg deps with
| None -> ()
| Some pkgs -> OpamPackage.Set.iter f pkgs
end

module PkgTopo = Graph.Topological.Make (PkgGraph)

let dependency_cone_sizes depends =
(* packages in reverse topological order *)
let pkgs = PkgTopo.fold (fun pkg acc -> pkg :: acc) depends [] in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ah, yeah i was originally thinking of a brute force approach + hashtable to avoid recomputation, but it is 25x slower if you take the whole universe. The topological graph approach is clever, neat approximation.

Comment thread src/o2wUniverse.ml
OpamPackage.Map.add pkg cone acc)
OpamPackage.Map.empty pkgs
in
OpamPackage.Map.map OpamPackage.Name.Set.cardinal cones

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To avoid having every almost package start at 10 dependencies, we could cheat a little and only show the number of dependencies starting from the ocaml package. What do you think?

Suggested change
OpamPackage.Map.map OpamPackage.Name.Set.cardinal cones
let ocaml_cardinal =
OpamPackage.Name.Set.cardinal
(OpamPackage.Map.find (OpamPackage.of_string "ocaml.5.5.0") cones)
in
OpamPackage.Map.map (fun set ->
Int.max 0 (OpamPackage.Name.Set.cardinal set - ocaml_cardinal))
cones

@ferminr ferminr Aug 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That's a good suggestion. But we should not substract ocaml_cardinal unconditionally from every cone, only from those with pacakges which included ocaml explicitly as a dependency. (If we did, many packages would end up with a cone of size < 0). Instead of updating cardinalities I've removed the ocaml dependency earlier in the calculation.

An interesting example. Inspecting the dependency cone of alcobar (opam list --rec --required-by "alcobar.0.3.1") we see jbuilder. I asked an LLM for an explanation, and I got this back:

alcobar depends on alcotest with no version constraint, so that resolves to every alcotest in the repository — 1.0.1 through 1.9.1. alcotest.1.0.1 is a 2020 release, and it wants re {>= "1.7.2"}, which again is open-ended upward but reaches down to re.1.7.2 — a 2018 release, from back when re was built with jbuilder, dune's predecessor.

It further suggests:

If you wanted the number closer to "what would actually get installed", the lever is picking a single version per dependency name — the latest satisfying one, say — rather than all of them. That's a change in cone_depends, where OpamFormula.packages currently returns the full satisfying set.

Not planning to do anything about it in this PR, since opam list behaves the same way.

@ferminr

ferminr commented Aug 15, 2026

Copy link
Copy Markdown
Author

button to sort by number of rev-deps

done in commit bde1fb7

@ferminr
ferminr requested a review from kit-ty-kate August 16, 2026 08:47
@kit-ty-kate

Copy link
Copy Markdown
Member

I don't have too much time for this particular review this week but i've at least pushed it to live-staging to see the result live on the staging website. It should be deployed there soon.

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.

2 participants