Skip to content

Commit e7658d2

Browse files
authored
Merge pull request #33268 from vespa-engine/havardpe/select-slowest-search-thread
select slowest search thread in vespa inspect profile
2 parents 6b0ac89 + 5b9ddb3 commit e7658d2

File tree

6 files changed

+124
-54
lines changed

6 files changed

+124
-54
lines changed

client/go/internal/cli/cmd/inspect.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,34 @@ func inspectProfile(cli *CLI, opts *inspectProfileOptions) error {
2323
if !root.Valid() {
2424
return fmt.Errorf("profile file '%s' does not contain valid JSON", opts.profileFile)
2525
}
26-
var cnt int
27-
var maxTime float64
28-
var maxTrace *tracedoctor.ProtonTrace = nil
29-
for _, trace := range tracedoctor.FindProtonTraces(root.Field("trace")) {
30-
cnt++
31-
if trace.DurationMs > maxTime {
32-
maxTime = trace.DurationMs
33-
maxTrace = trace
26+
list := tracedoctor.FindProtonTraces(root.Field("trace"))
27+
if len(list) == 0 {
28+
return fmt.Errorf("could not locate any searches in profile(%s)", opts.profileFile)
29+
}
30+
var mp int = -1
31+
for i, p := range list {
32+
if mp < 0 || p.DurationMs() > list[mp].DurationMs() {
33+
mp = i
34+
}
35+
}
36+
fmt.Fprintf(cli.Stdout, "found %d searches, slowest search was: %s[%d]: %10.3f ms\n",
37+
len(list), list[mp].DocumentType(), list[mp].DistributionKey(), list[mp].DurationMs())
38+
threads := list[mp].FindThreadTraces()
39+
if len(threads) == 0 {
40+
return fmt.Errorf("search thread information is missing from profile(%s)", opts.profileFile)
41+
}
42+
var mt int = -1
43+
for i, t := range threads {
44+
if mt < 0 || t.MatchTimeMs() > threads[mt].MatchTimeMs() {
45+
mt = i
3446
}
3547
}
36-
if maxTrace != nil {
37-
fmt.Fprintf(cli.Stdout, "found %d searches, slowest search was: %s[%d]: %10.3f ms\n",
38-
cnt, maxTrace.DocumentType, maxTrace.DistributionKey, maxTrace.DurationMs)
39-
return maxTrace.MatchProfiling().Render(cli.Stdout)
48+
fmt.Fprintf(cli.Stdout, "found %d threads, slowest matching was thread #%d: %10.3f ms\n",
49+
len(threads), mt, threads[mt].MatchTimeMs())
50+
queryPerf := list[mp].ExtractQuery()
51+
queryPerf.ImportMatchPerf(threads[mt])
52+
if err := queryPerf.Render(cli.Stdout); err != nil {
53+
return err
4054
}
4155
return nil
4256
}

client/go/internal/cli/cmd/query.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func query(cli *CLI, arguments []string, opts *queryOptions, waiter *Waiter) err
118118
urlQuery.Set("trace.level", "1")
119119
urlQuery.Set("trace.explainLevel", "1")
120120
urlQuery.Set("trace.profiling.matching.depth", "100")
121+
urlQuery.Set("presentation.timing", "true")
121122
}
122123
queryTimeout := urlQuery.Get("timeout")
123124
if queryTimeout == "" {

client/go/internal/vespa/slime/leaf_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,14 @@ func TestData(t *testing.T) {
7474
checkLeaf(t, Data([]byte{1, 2, 3}), expectLeaf{mytype: DATA, dataVal: []byte{1, 2, 3}})
7575
checkLeaf(t, Data([]byte{5, 6}), expectLeaf{mytype: DATA, dataVal: []byte{5, 6}})
7676
}
77+
78+
func TestValidMultiPredicate(t *testing.T) {
79+
a := Invalid
80+
b := Bool(true)
81+
c := Long(42)
82+
assert.False(t, Valid(a, b, c))
83+
assert.False(t, Valid(b, a, c))
84+
assert.True(t, Valid(b, c))
85+
assert.True(t, Valid(c))
86+
assert.True(t, Valid())
87+
}

client/go/internal/vespa/slime/path.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,8 @@ func (p *Path) Clone() *Path {
111111
return &Path{slices.Clone(p.list)}
112112
}
113113

114-
func Find(value Value, pred func(path *Path, value Value) bool) []*Path {
114+
func Select(value Value, pred func(p *Path, v Value) bool, handle func(p *Path, v Value)) {
115115
path := NewPath()
116-
var result []*Path
117116
var process func(value Value)
118117
perEntry := func(idx int, value Value) {
119118
path.Entry(idx)
@@ -127,12 +126,19 @@ func Find(value Value, pred func(path *Path, value Value) bool) []*Path {
127126
}
128127
process = func(value Value) {
129128
if pred(path, value) {
130-
result = append(result, path.Clone())
129+
handle(path, value)
131130
return
132131
}
133132
value.EachEntry(perEntry)
134133
value.EachField(perField)
135134
}
136135
process(value)
136+
}
137+
138+
func Find(value Value, pred func(path *Path, value Value) bool) []*Path {
139+
var result []*Path
140+
Select(value, pred, func(p *Path, v Value) {
141+
result = append(result, p.Clone())
142+
})
137143
return result
138144
}

client/go/internal/vespa/slime/path_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,17 @@ func TestFindMultipleFields(t *testing.T) {
105105
verifyArray([]verifyValue{verifyLong(9), verifyLong(10)})(t, arr2)
106106
verifyArray([]verifyValue{verifyLong(3), verifyLong(4), verifyLong(5)})(t, arr3)
107107
}
108+
109+
func TestSelectProcessing(t *testing.T) {
110+
pred := func(p *Path, v Value) bool { return p.At(-1).WouldSelectField("y") }
111+
handle := func(p *Path, v Value) {
112+
assert.True(t, p.At(-1).WouldSelectField("y"))
113+
if v.NumEntries() == 2 {
114+
verifyArray([]verifyValue{verifyLong(9), verifyLong(10)})(t, v)
115+
} else {
116+
assert.Equal(t, 3, v.NumEntries())
117+
verifyArray([]verifyValue{verifyLong(3), verifyLong(4), verifyLong(5)})(t, v)
118+
}
119+
}
120+
Select(createComplexValue(t), pred, handle)
121+
}

client/go/internal/vespa/tracedoctor/extract.go

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -204,24 +204,10 @@ func (q *queryNode) applySample(sample slime.Value) {
204204
}
205205
}
206206

207-
type ProtonTrace struct {
208-
DistributionKey int64
209-
DocumentType string
210-
DurationMs float64
211-
Root slime.Value
212-
Query slime.Value
213-
}
214-
215-
func findField(node slime.Value, fieldName string) []*slime.Path {
216-
return slime.Find(node, func(path *slime.Path, value slime.Value) bool {
217-
return path.At(-1).WouldSelectField(fieldName)
218-
})
219-
}
220-
221-
func findTagged(node slime.Value, tag string) []*slime.Path {
222-
return slime.Find(node, func(path *slime.Path, value slime.Value) bool {
223-
return value.Field("tag").AsString() == tag
224-
})
207+
func hasTag(tag string) func(p *slime.Path, v slime.Value) bool {
208+
return func(p *slime.Path, v slime.Value) bool {
209+
return v.Field("tag").AsString() == tag
210+
}
225211
}
226212

227213
func eachSampleList(list slime.Value, f func(sample slime.Value)) {
@@ -235,33 +221,71 @@ func eachSample(prof slime.Value, f func(sample slime.Value)) {
235221
eachSampleList(prof.Field("roots"), f)
236222
}
237223

238-
func (pt *ProtonTrace) MatchProfiling() *queryNode {
239-
queryRoot := extractQueryNode(pt.Query)
240-
for _, profPath := range findTagged(pt.Root, "match_profiling") {
241-
prof := profPath.Apply(pt.Root)
242-
if prof.Field("profiler").AsString() == "tree" {
243-
eachSample(prof, func(sample slime.Value) {
244-
if sampleType(sample) == "seek" {
245-
queryRoot.applySample(sample)
224+
func (q *queryNode) ImportMatchPerf(t ThreadTrace) {
225+
slime.Select(t.root, hasTag("match_profiling"), func(p *slime.Path, v slime.Value) {
226+
if v.Field("profiler").AsString() == "tree" {
227+
eachSample(v, func(sample slime.Value) {
228+
if sampleType(sample) != "unknown" {
229+
q.applySample(sample)
246230
}
247231
})
248232
}
233+
})
234+
}
235+
236+
type ThreadTrace struct {
237+
root slime.Value
238+
}
239+
240+
func (t ThreadTrace) MatchTimeMs() float64 {
241+
p := slime.Find(t.root, hasTag("match_profiling"))
242+
if len(p) == 1 {
243+
return p[0].Apply(t.root).Field("total_time_ms").AsDouble()
249244
}
250-
return queryRoot
245+
return 0.0
251246
}
252247

253-
func FindProtonTraces(root slime.Value) []*ProtonTrace {
254-
var traces []*ProtonTrace
255-
for _, path := range findField(root, "optimized") {
256-
node := path.Clone().Trim(3).Apply(root)
257-
distKey := node.Field("distribution-key")
258-
docType := node.Field("document-type")
259-
duration := node.Field("duration_ms")
260-
if slime.Valid(distKey, docType, duration) {
261-
traces = append(traces, &ProtonTrace{distKey.AsLong(),
262-
docType.AsString(),
263-
duration.AsDouble(), node, path.Apply(root)})
264-
}
248+
type ProtonTrace struct {
249+
root slime.Value
250+
}
251+
252+
func (p ProtonTrace) FindThreadTraces() []ThreadTrace {
253+
var traces []ThreadTrace
254+
slime.Select(p.root, hasTag("query_execution"), func(p *slime.Path, v slime.Value) {
255+
v.Field("threads").EachEntry(func(idx int, v slime.Value) {
256+
traces = append(traces, ThreadTrace{v})
257+
})
258+
})
259+
return traces
260+
}
261+
262+
func (p ProtonTrace) ExtractQuery() *queryNode {
263+
query := slime.Invalid
264+
plan := slime.Find(p.root, hasTag("query_execution_plan"))
265+
if len(plan) == 1 {
266+
query = plan[0].Apply(p.root).Field("optimized")
265267
}
268+
return extractQueryNode(query)
269+
}
270+
271+
func (p ProtonTrace) DistributionKey() int64 {
272+
return p.root.Field("distribution-key").AsLong()
273+
}
274+
275+
func (p ProtonTrace) DocumentType() string {
276+
return p.root.Field("document-type").AsString()
277+
}
278+
279+
func (p ProtonTrace) DurationMs() float64 {
280+
return p.root.Field("duration_ms").AsDouble()
281+
}
282+
283+
func FindProtonTraces(root slime.Value) []ProtonTrace {
284+
var traces []ProtonTrace
285+
slime.Select(root, func(p *slime.Path, v slime.Value) bool {
286+
return slime.Valid(v.Field("distribution-key"), v.Field("document-type"), v.Field("duration_ms"))
287+
}, func(p *slime.Path, v slime.Value) {
288+
traces = append(traces, ProtonTrace{v})
289+
})
266290
return traces
267291
}

0 commit comments

Comments
 (0)