Skip to content

Commit 5729a1c

Browse files
committed
treesteps: allow passthrough
This change improves tresteps to allow eliding nodes like `invalidating.Iter` or `keyspan.assertIter` from the hierarchy. If we simply pass through the `NodeInfo` from the wrapped node, we still register the elided node instead of the wrapped node (and thus the wrapped node ops are not recorded). To fix this, we the `Node` to `NodeInfo`. Note that passthrough happens implicitly when a node embeds another node and relies on the embedded `TreeSpepsNode()` implementation (like `colblk.KeyspanIter` embeds `colblk.keyspanIter`).
1 parent 7b52a87 commit 5729a1c

File tree

4 files changed

+115
-30
lines changed

4 files changed

+115
-30
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Here B is wrapped inside an invisible node; verify that we see the B op.
2+
run
3+
----
4+
step 1/5:
5+
A
6+
└── B
7+
step 2/5:
8+
A ← Op
9+
└── B
10+
step 3/5:
11+
A ← Op
12+
└── B ← Op
13+
step 4/5:
14+
A ← Op
15+
└── B ← Op done
16+
step 5/5:
17+
A ← Op done
18+
└── B

internal/treesteps/tree_steps_off.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type Node interface {
2323
}
2424
type NodeInfo struct{}
2525

26-
func NodeInfof(format string, args ...any) NodeInfo { return NodeInfo{} }
26+
func NodeInfof(node Node, format string, args ...any) NodeInfo { return NodeInfo{} }
2727
func (ni *NodeInfo) AddPropf(key string, format string, args ...any) {}
2828
func (ni *NodeInfo) AddChildren(nodes ...Node) {}
2929

internal/treesteps/tree_steps_on.go

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,21 @@ func StartRecording(root Node, name string, opts ...RecordingOption) *Recording
3636
mu.Lock()
3737
defer mu.Unlock()
3838
mu.recordingInProgress.Store(true)
39-
w := &Recording{
39+
rec := &Recording{
4040
name: name,
4141
maxTreeDepth: 20,
4242
maxOpDepth: 10,
43+
root: root,
4344
}
4445
for _, o := range opts {
45-
o(w)
46+
o(rec)
4647
}
4748

4849
if mu.nodeMap == nil {
4950
mu.nodeMap = make(map[Node]*nodeState)
5051
}
51-
w.root = nodeStateLocked(w, root)
52-
w.stepLockedf("initial")
53-
return w
52+
rec.stepLockedf("initial")
53+
return rec
5454
}
5555

5656
// RecordingOption is an optional argument to StartRecording.
@@ -106,20 +106,27 @@ func NodeUpdated(n Node, reason string) {
106106

107107
// Node must be implemented by every node in the hierarchy.
108108
type Node interface {
109+
// TreeStepsNode returns the information about the current state of the node.
110+
// The NodeInfo normally has the receiver as the Node; but it can be a
111+
// descendant in the case of passthrough nodes.
109112
TreeStepsNode() NodeInfo
110113
}
111114

112115
// NodeInfo contains the information that we present for each node.
113116
type NodeInfo struct {
117+
node Node
114118
name string
115119
properties [][2]string
116120
children []Node
117121
}
118122

119123
// NodeInfof returns a NodeInfo with the name initialized with a formatted
120124
// string.
121-
func NodeInfof(format string, args ...any) NodeInfo {
122-
return NodeInfo{name: fmt.Sprintf(format, args...)}
125+
func NodeInfof(node Node, format string, args ...any) NodeInfo {
126+
return NodeInfo{
127+
node: node,
128+
name: fmt.Sprintf(format, args...),
129+
}
123130
}
124131

125132
// AddPropf adds a property to the NodeInfo.
@@ -148,7 +155,7 @@ type Recording struct {
148155
name string
149156
maxTreeDepth int
150157
maxOpDepth int
151-
root *nodeState
158+
root Node
152159
steps []Step
153160
}
154161

@@ -173,7 +180,7 @@ func (r *Recording) Finish() Steps {
173180
func (r *Recording) stepLockedf(format string, args ...any) {
174181
r.steps = append(r.steps, Step{
175182
Name: fmt.Sprintf(format, args...),
176-
Root: buildTree(r.root),
183+
Root: buildTree(r, r.root, 0),
177184
})
178185
}
179186

@@ -328,39 +335,43 @@ var mu struct {
328335
nodeMap map[Node]*nodeState
329336
}
330337

331-
func buildTree(n *nodeState) TreeNode {
332-
var t TreeNode
333-
info := n.node.TreeStepsNode()
334-
n.name = info.name
335-
t.Name = info.name
336-
t.Properties = info.properties
337-
if len(n.ops) > 0 {
338-
t.Ops = make([]string, len(n.ops))
338+
func buildTree(rec *Recording, n Node, depth int) TreeNode {
339+
info := n.TreeStepsNode()
340+
// Normally these are the same; they differ n is a "passthrough" node.
341+
n = info.node
342+
ns := nodeStateLocked(rec, n)
343+
ns.name = info.name
344+
ns.depth = depth
345+
346+
t := TreeNode{
347+
Name: info.name,
348+
Properties: info.properties,
349+
}
350+
if len(ns.ops) > 0 {
351+
t.Ops = make([]string, len(ns.ops))
339352
for i := range t.Ops {
340-
t.Ops[i] = n.ops[i].details
341-
if n.ops[i].state != "" {
342-
t.Ops[i] += " " + n.ops[i].state
353+
t.Ops[i] = ns.ops[i].details
354+
if ns.ops[i].state != "" {
355+
t.Ops[i] += " " + ns.ops[i].state
343356
}
344357
}
345358
}
346-
if n.depth < n.recording.maxTreeDepth {
347-
for i := range info.children {
348-
c := nodeStateLocked(n.recording, info.children[i])
349-
c.depth = n.depth + 1
350-
t.Children = append(t.Children, buildTree(c))
359+
if depth < rec.maxTreeDepth {
360+
for _, c := range info.children {
361+
t.Children = append(t.Children, buildTree(rec, c, depth+1))
351362
}
352363
} else if len(info.children) > 0 {
353364
t.HasHiddenChildren = true
354365
}
355366
return t
356367
}
357368

358-
func nodeStateLocked(w *Recording, n Node) *nodeState {
369+
func nodeStateLocked(rec *Recording, n Node) *nodeState {
359370
ns, ok := mu.nodeMap[n]
360371
if !ok {
361-
ns = &nodeState{recording: w, node: n}
372+
ns = &nodeState{recording: rec, node: n}
362373
mu.nodeMap[n] = ns
363-
} else if w != ns.recording {
374+
} else if rec != ns.recording {
364375
panic(fmt.Sprintf("node %v part of multiple recordings", n))
365376
}
366377
return ns

internal/treesteps/tree_steps_test.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ var _ Node = (*SegmentNode)(nil)
7070

7171
// TreeStepsNode implements the Node interface.
7272
func (n *SegmentNode) TreeStepsNode() NodeInfo {
73-
info := NodeInfof("n%d", n.Index)
73+
info := NodeInfof(n, "n%d", n.Index)
7474
info.AddPropf("range", "[%d, %d]", n.Start, n.End)
7575
info.AddPropf("sum", "%d", n.Sum)
7676
if n.Start < n.End {
@@ -178,3 +178,59 @@ func TestSegmentTree(t *testing.T) {
178178
return ""
179179
})
180180
}
181+
182+
// TestSegmentTreePassthrough verifies that operations work as expected when the
183+
// hierarchy has passthrough nodes.
184+
func TestSegmentTreePassthrough(t *testing.T) {
185+
if !Enabled {
186+
t.Skip("treesteps not available in this build")
187+
}
188+
datadriven.RunTest(t, "testdata/passthrough", func(t *testing.T, td *datadriven.TestData) string {
189+
a := &nodeA{child: &nodeBWrapper{}}
190+
r := StartRecording(a, "foo")
191+
a.Op()
192+
steps := r.Finish()
193+
return steps.String()
194+
})
195+
}
196+
197+
type nodeA struct {
198+
child interface {
199+
Node
200+
Op()
201+
}
202+
}
203+
204+
func (n *nodeA) TreeStepsNode() NodeInfo {
205+
ni := NodeInfof(n, "A")
206+
ni.AddChildren(n.child)
207+
return ni
208+
}
209+
210+
func (n *nodeA) Op() {
211+
if IsRecording(n) {
212+
op := StartOpf(n, "Op")
213+
defer op.Finishf("done")
214+
}
215+
n.child.Op()
216+
}
217+
218+
type nodeB struct{}
219+
220+
// nodeBWrapper is a passthrough node. When `(*nodeBWrapper).TreeStepsNode()` is called,
221+
// it is executed on `(*nodeB)`. Operations in `nodeB` would be associated with
222+
// the `*nodeB` node, not the `(*nodeBWrapper)` node.
223+
type nodeBWrapper struct {
224+
nodeB
225+
}
226+
227+
func (n *nodeB) Op() {
228+
if IsRecording(n) {
229+
op := StartOpf(n, "Op")
230+
defer op.Finishf("done")
231+
}
232+
}
233+
234+
func (n *nodeB) TreeStepsNode() NodeInfo {
235+
return NodeInfof(n, "B")
236+
}

0 commit comments

Comments
 (0)