Files
kenn-io--agentsview/internal/db/search_content_bench_test.go
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

175 lines
6.8 KiB
Go

package db
import (
"context"
"fmt"
"io"
"log"
"testing"
)
// Pre-change baseline for content-search page fetches. CI's bench-gate
// workflow will compare these against a candidate once per-match citation
// derivation lands on top of SearchContent (see the conversation-unit
// citations design), so a regression in the derivation cost shows up as an
// ns/op or allocs/op delta on a PR instead of shipping silently.
//
// The corpus is built once per benchmark (outside the timed loop) to stress
// exactly what citation derivation has to walk: long assistant monologues,
// system rows breaking up a run without ending it, and sidechain stretches
// that do end a run. See seedContentSearchBench for the exact shape.
const (
benchContentSessions = 40
benchContentMessages = 300
// benchContentRunStart and benchContentRunEnd bound the one long
// assistant run per session (inclusive ordinals).
benchContentRunStart = 10
benchContentRunEnd = 260
// benchContentSegment is both the system-row cadence inside the run and
// the sidechain-stretch width: every 50th ordinal inside the run (not at
// its start or end) is a system row, and the assistant messages between
// consecutive system rows alternate is_sidechain, so the run contains
// multiple contiguous sidechain stretches rather than one flat run.
benchContentSegment = 50
)
// seedContentSearchBench builds a corpus that stresses the citation
// derivation: long assistant runs (the monologue case), sidechain
// stretches, system rows inside runs, and a term that matches broadly.
// 40 sessions x 300 messages; in each session ordinals 10..260 form one
// assistant run (with a system row every 50 ordinals inside it), the rest
// alternate user/assistant. Every assistant message contains "needle";
// IN-RUN assistant messages additionally contain "runneedle", so the
// rank-ordered FTS benchmark can pin its page to the long-run region
// instead of filling with outside-run hits.
func seedContentSearchBench(b *testing.B, d *DB) {
b.Helper()
origLog := log.Writer()
log.SetOutput(io.Discard)
b.Cleanup(func() { log.SetOutput(origLog) })
for i := range benchContentSessions {
sessionID := fmt.Sprintf("bench-search-%03d", i)
if err := d.UpsertSession(Session{
ID: sessionID, Project: "bench", Machine: "local", Agent: "claude",
// MessageCount > 0 so buildSessionFilter's base
// "message_count > 0" predicate keeps the session; UserMessageCount
// > 1 so the one-shot/automated exclusion in contentSessionFilter
// does not drop it either.
MessageCount: benchContentMessages,
UserMessageCount: 2,
}); err != nil {
b.Fatalf("seed session %s: %v", sessionID, err)
}
msgs := benchContentSearchMessages(sessionID)
if err := d.InsertMessages(msgs); err != nil {
b.Fatalf("seed messages for %s: %v", sessionID, err)
}
}
}
// benchContentSearchMessages builds the benchContentMessages-message
// timeline for one session per seedContentSearchBench's shape.
func benchContentSearchMessages(sessionID string) []Message {
msgs := make([]Message, 0, benchContentMessages)
for i := range benchContentMessages {
msgs = append(msgs, benchContentSearchMessage(sessionID, i))
}
return msgs
}
// benchContentSearchMessage builds the single message at ordinal in
// sessionID: a system row or sidechain-tagged assistant turn inside the
// long run (benchContentRunStart..benchContentRunEnd), or an alternating
// user/assistant message outside it. Every assistant message contains
// "needle".
func benchContentSearchMessage(sessionID string, ordinal int) Message {
ts := fmt.Sprintf("2026-06-%02dT10:00:00Z", 1+ordinal%28)
if ordinal >= benchContentRunStart && ordinal <= benchContentRunEnd {
return benchContentRunMessage(sessionID, ordinal, ts)
}
if ordinal%2 == 1 {
content := fmt.Sprintf(
"assistant reply %d contains needle outside the monologue run", ordinal,
)
return Message{
SessionID: sessionID, Ordinal: ordinal, Role: "assistant",
Content: content, Timestamp: ts, Model: "claude-bench-model",
ContentLength: len(content),
}
}
content := fmt.Sprintf("user message %d asking an unrelated question", ordinal)
return Message{
SessionID: sessionID, Ordinal: ordinal, Role: "user",
Content: content, Timestamp: ts, ContentLength: len(content),
}
}
// benchContentRunMessage builds one message inside the long assistant run:
// a system row on interior segment boundaries, otherwise an assistant
// message tagged is_sidechain for alternating benchContentSegment-wide
// stretches.
func benchContentRunMessage(sessionID string, ordinal int, ts string) Message {
offset := ordinal - benchContentRunStart
runLen := benchContentRunEnd - benchContentRunStart
if offset > 0 && offset < runLen && offset%benchContentSegment == 0 {
content := fmt.Sprintf(
"system notice at ordinal %d inside the assistant run", ordinal,
)
return Message{
SessionID: sessionID, Ordinal: ordinal, Role: "system",
Content: content, Timestamp: ts, IsSystem: true,
ContentLength: len(content),
}
}
content := fmt.Sprintf(
"assistant monologue turn %d contains needle and runneedle for the search benchmark",
ordinal,
)
return Message{
SessionID: sessionID, Ordinal: ordinal, Role: "assistant",
Content: content, Timestamp: ts, Model: "claude-bench-model",
IsSidechain: (offset/benchContentSegment)%2 == 1,
ContentLength: len(content),
}
}
// BenchmarkSearchContentSubstringPage measures a full 50-hit substring
// content-search page over benchContentSessions sessions, each with a
// "needle" in every assistant message -- the same broad-match, worst-case
// shape citation derivation has to walk per match.
func BenchmarkSearchContentSubstringPage(b *testing.B) {
d := testDB(b)
seedContentSearchBench(b, d)
f := ContentSearchFilter{Pattern: "needle", Limit: 50}
b.ReportAllocs()
b.ResetTimer()
for range b.N {
page, err := d.SearchContent(context.Background(), f)
if err != nil || len(page.Matches) != 50 {
b.Fatalf("search: %v (%d matches, want a full 50-hit page)", err, len(page.Matches))
}
}
}
// BenchmarkSearchContentFTSPage is BenchmarkSearchContentSubstringPage's
// FTS-mode counterpart, over the identical corpus. It searches the run-only
// term "runneedle": FTS orders by rank, so a broad term would fill the
// 50-hit page with outside-run hits and never exercise the long-run
// derivation shape this benchmark exists to measure.
func BenchmarkSearchContentFTSPage(b *testing.B) {
d := testDB(b)
if !d.HasFTS() {
b.Skip("fts5 not available")
}
seedContentSearchBench(b, d)
f := ContentSearchFilter{Pattern: "runneedle", Mode: "fts", Limit: 50}
b.ReportAllocs()
b.ResetTimer()
for range b.N {
page, err := d.SearchContent(context.Background(), f)
if err != nil || len(page.Matches) != 50 {
b.Fatalf("search: %v (%d matches, want a full 50-hit page)", err, len(page.Matches))
}
}
}