forked from kenn-io/agentsview
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmessages_bench_test.go
More file actions
159 lines (149 loc) · 4.68 KB
/
Copy pathmessages_bench_test.go
File metadata and controls
159 lines (149 loc) · 4.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
package db
import (
"encoding/json"
"fmt"
"io"
"log"
"testing"
)
// Hot-path benchmarks for the message write and usage aggregation
// paths that have regressed in the past. CI's bench-gate workflow
// runs them on every PR and compares allocs/op, B/op, and ns/op
// against the merge base, so a reintroduced O(session-history)
// rewrite or per-row JSON parse fails the PR instead of shipping:
//
// - BenchmarkReplaceSessionMessagesStreamingMerge: a one-row tail
// change must take the in-place diff path (one UPDATE) rather
// than delete+reinserting every row and rewriting the FTS index
// (regressed pre-#954: streaming chunk merges rewrote whole
// sessions on every appended chunk).
// - BenchmarkInsertMessagesBatch: bulk ingest must keep multi-row
// batched inserts (#411).
//
// BenchmarkGetDailyUsage in usage_test.go covers the usage
// aggregation scan (#309) and is part of the same CI gate.
// benchSessionMessages builds n alternating user/assistant messages.
// Assistant messages carry a model and token_usage payload so writes
// exercise the same columns real ingest does.
func benchSessionMessages(sessionID string, n int) []Message {
msgs := make([]Message, 0, n)
for i := range n {
role := "user"
content := fmt.Sprintf(
"user message %d with enough text to look real", i,
)
if i%2 == 1 {
role = "assistant"
content = fmt.Sprintf(
"assistant reply %d with enough text to look real", i,
)
}
m := Message{
SessionID: sessionID,
Ordinal: i,
Role: role,
Content: content,
Timestamp: fmt.Sprintf("2026-06-%02dT10:00:00Z", 1+i%28),
ContentLength: len(content),
}
if role == "assistant" {
m.Model = "claude-bench-model"
m.TokenUsage = json.RawMessage(
`{"input_tokens":120,"output_tokens":45,` +
`"cache_creation_input_tokens":10,` +
`"cache_read_input_tokens":200}`,
)
}
msgs = append(msgs, m)
}
return msgs
}
func seedBenchSession(
b *testing.B, d *DB, sessionID string, n int,
) []Message {
b.Helper()
if err := d.UpsertSession(Session{
ID: sessionID,
Project: "bench",
Machine: "local",
Agent: "claude",
}); err != nil {
b.Fatalf("seed session %s: %v", sessionID, err)
}
msgs := benchSessionMessages(sessionID, n)
if err := d.InsertMessages(msgs); err != nil {
b.Fatalf("seed messages for %s: %v", sessionID, err)
}
return msgs
}
// BenchmarkReplaceSessionMessagesStreamingMerge measures the
// streaming chunk-merge shape: replacing a stored session where only
// the tail message's content changed. The diff planner must apply a
// single in-place UPDATE; cost must not scale with the number of
// unchanged stored rows being rewritten.
func BenchmarkReplaceSessionMessagesStreamingMerge(b *testing.B) {
silenceBenchmarkLogs(b)
const stored = 1000
d := testDB(b)
msgs := seedBenchSession(b, d, "bench-replace", stored)
last := len(msgs) - 1
b.ReportAllocs()
b.ResetTimer()
for i := range b.N {
content := fmt.Sprintf(
"assistant reply %d merged streaming tail variant %d",
last, i,
)
msgs[last].Content = content
msgs[last].ContentLength = len(content)
if err := d.ReplaceSessionMessages("bench-replace", msgs); err != nil {
b.Fatalf("replace: %v", err)
}
}
}
// BenchmarkInsertMessagesBatch measures bulk session ingest: one
// session row plus a batch insert of its messages, the unit of work
// the full-sync write pipeline performs per session.
//
// Each iteration adds a new session, so the database grows with the
// iteration count and per-op cost is only comparable between runs
// with the same count: the bench gate always runs with a fixed
// -benchtime=Nx (see bench.yml and the Makefile) so baseline and
// candidate insert into identically sized databases.
func BenchmarkInsertMessagesBatch(b *testing.B) {
silenceBenchmarkLogs(b)
const batch = 200
d := testDB(b)
// Build the message fixture once: constructing 200 Message
// structs (~400 fmt.Sprintf calls) inside the timed loop would
// be gated as if it were ingest cost. Only the SessionID is
// rewritten per iteration, which allocates nothing.
msgs := benchSessionMessages("", batch)
b.ReportAllocs()
b.ResetTimer()
for i := range b.N {
sid := fmt.Sprintf("bench-insert-%06d", i)
if err := d.UpsertSession(Session{
ID: sid,
Project: "bench",
Machine: "local",
Agent: "claude",
}); err != nil {
b.Fatalf("upsert session: %v", err)
}
for j := range msgs {
msgs[j].SessionID = sid
}
if err := d.InsertMessages(msgs); err != nil {
b.Fatalf("insert messages: %v", err)
}
}
}
func silenceBenchmarkLogs(b *testing.B) {
b.Helper()
origLog := log.Writer()
log.SetOutput(io.Discard)
b.Cleanup(func() {
log.SetOutput(origLog)
})
}