Skip to content

Commit 4399d70

Browse files
committed
refactor: remove analyze.go, it is now observer_analyze.go and simplify
1 parent e5670ae commit 4399d70

File tree

5 files changed

+202
-137
lines changed

5 files changed

+202
-137
lines changed

pkg/query/analyze.go

Lines changed: 0 additions & 110 deletions
This file was deleted.

pkg/query/context.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package query
22

33
import (
44
"context"
5-
"time"
65

76
"github.com/authzed/spicedb/internal/caveats"
87
"github.com/authzed/spicedb/pkg/datalayer"
@@ -184,18 +183,15 @@ func (ctx *Context) notifyEnterIterator(op ObserverOperation, key CanonicalKey)
184183
}
185184
}
186185

187-
// wrapPathSeqWithObservers wraps a PathSeq to notify all registered observers of paths and timing.
186+
// wrapPathSeqWithObservers wraps a PathSeq to notify all registered observers of paths and completion.
188187
// Returns the original PathSeq unchanged when there are no observers.
189188
func (ctx *Context) wrapPathSeqWithObservers(op ObserverOperation, key CanonicalKey, pathSeq PathSeq) PathSeq {
190189
if len(ctx.observers) == 0 {
191190
return pathSeq
192191
}
193192
return func(yield func(Path, error) bool) {
194-
start := time.Now()
195193
defer func() {
196-
dur := time.Since(start)
197194
for _, obs := range ctx.observers {
198-
obs.ObserveTiming(op, key, dur)
199195
obs.ObserveReturnIterator(op, key)
200196
}
201197
}()

pkg/query/observer.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package query
22

3-
import "time"
4-
53
type ObserverOperation int
64

75
const (
@@ -11,8 +9,7 @@ const (
119
)
1210

1311
type Observer interface {
14-
ObserveTiming(op ObserverOperation, key CanonicalKey, dur time.Duration)
15-
ObservePath(op ObserverOperation, key CanonicalKey, path Path)
1612
ObserveEnterIterator(op ObserverOperation, key CanonicalKey)
13+
ObservePath(op ObserverOperation, key CanonicalKey, path Path)
1714
ObserveReturnIterator(op ObserverOperation, key CanonicalKey)
1815
}

pkg/query/observer_analyze.go

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package query
22

33
import (
4+
"fmt"
45
"maps"
6+
"strings"
57
"sync"
68
"time"
79
)
@@ -21,18 +23,20 @@ type AnalyzeStats struct {
2123

2224
// AnalyzeObserver is a thread-safe Observer that collects execution statistics keyed by CanonicalKey.
2325
type AnalyzeObserver struct {
24-
mu sync.Mutex
25-
stats map[CanonicalKey]AnalyzeStats // GUARDED_BY(mu)
26+
mu sync.Mutex
27+
stats map[CanonicalKey]AnalyzeStats // GUARDED_BY(mu)
28+
startTimes map[ObserverOperation]map[CanonicalKey]time.Time // GUARDED_BY(mu)
2629
}
2730

2831
// NewAnalyzeObserver creates a new thread-safe analyze observer.
2932
func NewAnalyzeObserver() *AnalyzeObserver {
3033
return &AnalyzeObserver{
31-
stats: make(map[CanonicalKey]AnalyzeStats),
34+
stats: make(map[CanonicalKey]AnalyzeStats),
35+
startTimes: make(map[ObserverOperation]map[CanonicalKey]time.Time),
3236
}
3337
}
3438

35-
// ObserveEnterIterator increments the call counter for the given iterator key and operation.
39+
// ObserveEnterIterator increments the call counter and records the start time.
3640
func (a *AnalyzeObserver) ObserveEnterIterator(op ObserverOperation, key CanonicalKey) {
3741
a.mu.Lock()
3842
defer a.mu.Unlock()
@@ -47,6 +51,11 @@ func (a *AnalyzeObserver) ObserveEnterIterator(op ObserverOperation, key Canonic
4751
stats.IterResourcesCalls++
4852
}
4953
a.stats[key] = stats
54+
55+
if a.startTimes[op] == nil {
56+
a.startTimes[op] = make(map[CanonicalKey]time.Time)
57+
}
58+
a.startTimes[op][key] = time.Now()
5059
}
5160

5261
// ObservePath increments the result counter for the given iterator key and operation.
@@ -66,26 +75,29 @@ func (a *AnalyzeObserver) ObservePath(op ObserverOperation, key CanonicalKey, _
6675
a.stats[key] = stats
6776
}
6877

69-
// ObserveTiming records the elapsed time for the given iterator key and operation.
70-
func (a *AnalyzeObserver) ObserveTiming(op ObserverOperation, key CanonicalKey, dur time.Duration) {
78+
// ObserveReturnIterator records elapsed time since ObserveEnterIterator was called.
79+
func (a *AnalyzeObserver) ObserveReturnIterator(op ObserverOperation, key CanonicalKey) {
7180
a.mu.Lock()
7281
defer a.mu.Unlock()
7382

74-
stats := a.stats[key]
75-
switch op {
76-
case CheckOperation:
77-
stats.CheckTime += dur
78-
case IterSubjectsOperation:
79-
stats.IterSubjectsTime += dur
80-
case IterResourcesOperation:
81-
stats.IterResourcesTime += dur
83+
if opTimes := a.startTimes[op]; opTimes != nil {
84+
if start, ok := opTimes[key]; ok {
85+
elapsed := time.Since(start)
86+
stats := a.stats[key]
87+
switch op {
88+
case CheckOperation:
89+
stats.CheckTime += elapsed
90+
case IterSubjectsOperation:
91+
stats.IterSubjectsTime += elapsed
92+
case IterResourcesOperation:
93+
stats.IterResourcesTime += elapsed
94+
}
95+
a.stats[key] = stats
96+
delete(opTimes, key)
97+
}
8298
}
83-
a.stats[key] = stats
8499
}
85100

86-
// ObserveReturnIterator is a no-op for the analyze observer.
87-
func (a *AnalyzeObserver) ObserveReturnIterator(_ ObserverOperation, _ CanonicalKey) {}
88-
89101
// GetStats returns a copy of all collected stats.
90102
func (a *AnalyzeObserver) GetStats() map[CanonicalKey]AnalyzeStats {
91103
a.mu.Lock()
@@ -95,3 +107,107 @@ func (a *AnalyzeObserver) GetStats() map[CanonicalKey]AnalyzeStats {
95107
maps.Copy(result, a.stats)
96108
return result
97109
}
110+
111+
// FormatAnalysis returns a formatted string showing the iterator tree with execution statistics
112+
// for each iterator. Stats are looked up by iterator canonical key from the analyze map.
113+
func FormatAnalysis(tree Iterator, analyze map[CanonicalKey]AnalyzeStats) string {
114+
if tree == nil {
115+
return "No iterator tree provided"
116+
}
117+
if len(analyze) == 0 {
118+
return "No analysis data available"
119+
}
120+
121+
var sb strings.Builder
122+
formatNode(tree, analyze, &sb, "", true)
123+
return sb.String()
124+
}
125+
126+
// AggregateAnalyzeStats combines all the analyze stats from a map into a single
127+
// aggregated AnalyzeStats. This is useful for getting total counts across all
128+
// iterators in a query execution.
129+
func AggregateAnalyzeStats(analyze map[CanonicalKey]AnalyzeStats) AnalyzeStats {
130+
var total AnalyzeStats
131+
for _, stats := range analyze {
132+
total.CheckCalls += stats.CheckCalls
133+
total.IterSubjectsCalls += stats.IterSubjectsCalls
134+
total.IterResourcesCalls += stats.IterResourcesCalls
135+
total.CheckResults += stats.CheckResults
136+
total.IterSubjectsResults += stats.IterSubjectsResults
137+
total.IterResourcesResults += stats.IterResourcesResults
138+
total.CheckTime += stats.CheckTime
139+
total.IterSubjectsTime += stats.IterSubjectsTime
140+
total.IterResourcesTime += stats.IterResourcesTime
141+
}
142+
return total
143+
}
144+
145+
// formatNode recursively formats a single iterator node and its children
146+
// depth parameter tracks how deep we are in the tree (0 = root)
147+
func formatNode(it Iterator, analyze map[CanonicalKey]AnalyzeStats, sb *strings.Builder, indent string, isLast bool) {
148+
if it == nil {
149+
return
150+
}
151+
152+
// Get iterator info from Explain()
153+
explain := it.Explain()
154+
iterKey := it.CanonicalKey()
155+
stats := analyze[iterKey]
156+
157+
// Draw tree branch for non-root nodes
158+
// Root nodes have both empty indent and are drawn without any prefix
159+
// Check if we're a child by seeing if we have any indentation OR if this is being called recursively
160+
isRoot := indent == "" && !strings.Contains(sb.String(), "Calls:")
161+
162+
if !isRoot {
163+
if isLast {
164+
sb.WriteString(indent + "└─ ")
165+
} else {
166+
sb.WriteString(indent + "├─ ")
167+
}
168+
}
169+
170+
// Write iterator name and key (truncated for readability)
171+
fmt.Fprintf(sb, "%s (Hash: %016x)\n", explain.Info, iterKey.Hash())
172+
173+
// Write stats with indentation
174+
statsIndent := indent
175+
if indent != "" {
176+
if isLast {
177+
statsIndent += " "
178+
} else {
179+
statsIndent += "│ "
180+
}
181+
}
182+
183+
// Format call counts
184+
sb.WriteString(statsIndent + fmt.Sprintf(" Calls: Check=%d, IterSubjects=%d, IterResources=%d\n",
185+
stats.CheckCalls, stats.IterSubjectsCalls, stats.IterResourcesCalls))
186+
187+
// Format result counts
188+
sb.WriteString(statsIndent + fmt.Sprintf(" Results: Check=%d, IterSubjects=%d, IterResources=%d\n",
189+
stats.CheckResults, stats.IterSubjectsResults, stats.IterResourcesResults))
190+
191+
// Format timing
192+
sb.WriteString(statsIndent + fmt.Sprintf(" Time: Check=%v, IterSubjects=%v, IterResources=%v\n",
193+
stats.CheckTime, stats.IterSubjectsTime, stats.IterResourcesTime))
194+
195+
// Recursively format children
196+
subIts := it.Subiterators()
197+
if len(subIts) > 0 {
198+
var childIndent string
199+
switch {
200+
case indent == "":
201+
// Root node - children get no indent prefix, but will show tree chars
202+
childIndent = ""
203+
case isLast:
204+
childIndent = indent + " "
205+
default:
206+
childIndent = indent + "│ "
207+
}
208+
209+
for i, subIt := range subIts {
210+
formatNode(subIt, analyze, sb, childIndent, i == len(subIts)-1)
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)