Repository navigation
resource: build sub-graphs from a node's dependents instead of pruning a clone - #6642
Conversation
A graph shaped like a robot with one remote: the remote and 50 resources on it, plus 60 local components and services. MarkReachability runs on every remote refresh pass, and SubGraphFrom whenever a resource's dependents are marked for update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g a clone SubGraphFrom cloned the whole graph, sorted it topologically, and removed every node that doesn't depend on the target. Each removal updates the transitive closure across the whole graph, so one call costs about V^2 per removed edge. MarkReachability went through the same path, and viam-server's remote refresh calls it on every pass, as does markChildrenForUpdate when a resource is rebuilt. On a Raspberry Pi CM5 with one remote this was about 6% of a core at idle. The target's dependents can be read straight from the transitive closure: the nodes n with closure[n][target] != 0, the same test the pruning loop used. The sub-graph is now built from those nodes: the same *GraphNode values, the edges among them, and the closure restricted to them. Every path between two dependents runs through dependents, so the closure among them is unchanged. MarkReachability marks the dependents directly, without building a sub-graph. A new test compares the result with the pruning approach (nodes, node pointers, children, parents, closure) on the existing test graph and on the robot-sized benchmark graph, and another checks MarkReachability marks exactly the node and its dependents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hey Jacob Kaplan (@qwertey6) — CI is green and no reviewer is assigned yet. Could you request one when you have a chance? Auto-comment from overwatch. Will not re-nudge for 7 days. |
|
Dan Gottlieb (@dgottlieb) could you review this when you have a chance? I can't request reviewers from a fork. You reviewed #5959's graph locking, and this is the next step for the same code path: |
| test.That(t, got.transitiveClosureMatrix[u][v], test.ShouldEqual, want.transitiveClosureMatrix[u][v]) | ||
| } | ||
| } | ||
| test.That(t, got.TopologicalSort(), test.ShouldHaveLength, len(want.nodes.nodes)) |
There was a problem hiding this comment.
I can see the utility of comparing to the pruning result to prove equivalence. Are there tests here that are actually exercising more cases than existing tests? If not, can just delete this.
But if there are cases worth covering, we should remove the subGraphByPruning and instead hard code the answers/assertions. That way a bug introduced in, say ReverseTopologicalSort has a predictable diagnosis rather than leaving us guess which way of generating a subgraph has a bug.
There was a problem hiding this comment.
Yes. They checked the sub-graph's edges and closure, which no existing test did. With the outside parents left in (D's edge to B, F's edge to A), every test in resource/ still passed. Node identity was already covered by TestResourceGraphMarkForRemoval.
So I dropped subGraphByPruning and the sweep in 749c542. TestResourceGraphSubGraph now hard-codes the sub-graph from C: children, parents (only C), and the closure among C, D and F. TestResourceGraphMarkReachability had the same problem, because it computed its expected marks from the closure. It now asserts the exact set: marking B marks B, C, D, E and F. Each assertion fails on a deliberately broken version: parents unfiltered, children dropped, closure rows not restricted, or every node marked.
| test.That(t, ok, test.ShouldBeTrue) | ||
| test.That(t, gotNode == wantNode, test.ShouldBeTrue) | ||
| } | ||
| test.That(t, edgeSets(got.children), test.ShouldResemble, edgeSets(want.children)) |
There was a problem hiding this comment.
What is edgeSets doing here? Getting subgraphs clones (ultimately) a map of keys, but the terminal values are the same pointer values. Do these assertions really fail without edgeSets?
Assuming we need to actually keep some part of this test that is.
There was a problem hiding this comment.
No, they pass without it. Both graphs hold the same &GraphNode{} edge values, so ShouldResemble on children and parents directly passes. It's gone along with the rest of that test.
|
|
||
| // The remote refresh marks the remote and everything depending on it | ||
| // reachable on every pass. | ||
| func BenchmarkGraphMarkReachability(b *testing.B) { |
There was a problem hiding this comment.
I really appreciate these. Very compelling results!
Drop the comparison against the old pruning approach. TestResourceGraphSubGraph now asserts the sub-graph's edges and transitive closure directly, and TestResourceGraphMarkReachability asserts the exact set of nodes it marks, so a failure points at the code under test rather than at one of two ways of building a sub-graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the MarkReachability conflict with viamrobotics#6653: mark the node and the remote resources among its dependents, read from the transitive closure, and leave local dependents alone. Drop TestResourceGraphMarkReachability. It asserted the old mark-every-dependent behavior, and TestStatusReachability from viamrobotics#6653 now asserts exactly which nodes MarkReachability marks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
For security reasons, this PR must be labeled with |
|
Dan Gottlieb (@dgottlieb) thanks! A change after your approval: #6653 landed and conflicted on The push removed the |
|
CI ran -- everything still looks good. Didn't look too closely at the post-merge conflict code. Push back if I ought to be more discerning. |
|
I don't think it needs a close look. The resolution is #6653's filter, applied to the dependents map instead of the sub-graph's nodes: for name, gNode := range dependents {
if name == node || name.Remote != "" {
gNode.markReachability(reachable)
}
}The rest of the merge is main's code, unchanged. |
Problem
Graph.SubGraphFromclones the whole graph, sorts it topologically, then removes every node that doesn't depend on the target. Each removal updates the transitive closure over the whole graph, so one call costs about V² per removed edge.Two hot paths go through it:
MarkReachability, which viam-server's remote refresh (updateRemoteResourceNames) calls on every pass, even when the remote is already reachable.markChildrenForUpdate, whenever a resource is rebuilt.On a Raspberry Pi CM5 running v1.11.3:
updateRemoteResourceNames → MarkReachability → subGraphFromWithMutexwas 5.8% of a core, 16% of viam-server's CPU (pprof).markChildrenForUpdate → SubGraphFromwas about half of viam-server's idle CPU.Change
nwithclosure[n][target] != 0, the same test the pruning loop used.*GraphNodevalues, the edges among them, and the closure restricted to them. Every path between two dependents runs through dependents, so the closure among them is unchanged.MarkReachabilityreads the dependents directly, without building a sub-graph, and marks the node and the remote resources among them (the behavior from APP-18274 - Report resource reachability in machine status #6653).Benchmark
The first commit adds
BenchmarkGraphMarkReachabilityandBenchmarkGraphSubGraphFromon a robot-sized graph: a remote with 50 resources plus 60 local components and services, ~110 nodes. Measured on a CM5, one core (taskset), 3 runs each, which agree within 1%. These runs predate merging #6653, whose filter adds one name check per dependent toMarkReachability:MarkReachabilitySubGraphFromTests
TestResourceGraphSubGraphnow also asserts the sub-graph's edges and transitive closure. The sub-graph from C keeps only the edges among C, D and F, not D's edge to B or F's edge to A. Before this, every test inresource/passed with those outside edges left in.TestStatusReachability(from APP-18274 - Report resource reachability in machine status #6653) asserts exactly which nodesMarkReachabilitymarks.SubGraphFrom. ForMarkReachability: every node marked, every dependent marked, only the node marked, or the node itself skipped.go test ./resource/...passes.robot/impltests covering remotes, offline remotes, reconfiguration, dependents and optional dependencies pass, includingTestReconfigure,TestRemoteRobotsUpdate,TestOfflineRemoteResources,TestRemovingOfflineRemotes,TestDependentAndOrphanedResourcesandTestModularOptionalDependencyOnRemote.robot/impltests APP-18274 - Report resource reachability in machine status #6653 changed (TestMachineStatus*,TestRemoteRobotsGold,TestInferRemoteRobotDependency*) pass: 16 tests.golangci-lintv2.13.2 reports no issues, with no format diff.🤖 Generated with Claude Code