Commit 35368fb
audit(runtime): dominator tree + retained size — which ONE reference, if cut, frees the memory
`roots` reports what holds the typical instance. It cannot tell you that CUTTING that
reference would free anything, because an object held by two references at once is
attributed to whichever is nearer. That is not an implementation shortcoming — "who holds
it" is ill-posed. The well-posed question is "which single reference, if cut, makes this
object collectable, and how much memory does that free", and dominance answers it: D
dominates X when EVERY path from a root to X goes through D, so D's retained size is what
you get back by dropping it. This is what Eclipse MAT and dotMemory are built on.
It immediately paid for itself on the SectorTS leak. `roots` names the static
PropertyChanged event, and it is not wrong — but `dominators` says:
>>> NO single reference holds this memory — the biggest dominator accounts for only
6.7% (24 MB of 361 MB). The objects are reachable from SEVERAL roots at once, so
cutting any one of them frees nothing.
Which is correct, and is exactly what the real fix turned out to be: the working fix
detaches SEVERAL references at once (UnregisterEventHandlers(false)). A shortest-path
walk would have named one, confidently, and the fix would not have worked.
Algorithm: Cooper-Harvey-Kennedy, "A Simple, Fast Dominance Algorithm" (2001) — the
iterative formulation LLVM used for years. A page of code, converges in a couple of
passes, no balanced forests. The paper is public; this is an implementation of it, not a
copy of anyone's code. (PerfView, MIT, is the closest .NET reference — it computes a
spanning tree with inclusive sizes, which approximates this.)
The graph is CSR, not List<List<int>>: ids are handed out in discovery order and BFS
processes nodes in that same order, so each node's successors land contiguously in one
int[]. ~150 bytes/object; 3.7M objects cost ~600 MB and 48 s attached live.
Correctness is not taken on faith:
* `RetentionPath selftest` (no target, no Windows, no ClrMD) checks the algorithm
against graphs whose dominators are known by hand — a DIAMOND (an object reachable
through both branches is dominated by NEITHER — the exact case a path walk gets
wrong), a CHAIN (retained size accumulates), and a CYCLE (a gate dominates a
reference cycle, which reference counting can never free). 13/13.
* At run time the super-root's retained size must equal the total size of the reachable
graph, or the walk REFUSES to report. A wrong dominator tree does not fail loudly — it
quietly tells you to cut the wrong reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjQnE1FDucd6vBVQbFswiE1 parent 25ba595 commit 35368fb
4 files changed
Lines changed: 584 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
115 | 169 | | |
116 | 170 | | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | 171 | | |
128 | 172 | | |
129 | 173 | | |
| |||
0 commit comments