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
492 lines
12 KiB
Go
492 lines
12 KiB
Go
//go:build benchdb
|
|
|
|
package backendbench
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/testcontainers/testcontainers-go"
|
|
tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
"go.kenn.io/agentsview/internal/duckdb"
|
|
"go.kenn.io/agentsview/internal/postgres"
|
|
)
|
|
|
|
const (
|
|
defaultBenchmarkSessionCount = 1000
|
|
defaultBenchmarkMessagesPerSession = 64
|
|
|
|
benchmarkMachine = "bench-machine"
|
|
benchmarkSchema = "agentsview_bench"
|
|
benchmarkQuery = "needle"
|
|
)
|
|
|
|
type benchmarkStore struct {
|
|
name string
|
|
store db.Store
|
|
}
|
|
|
|
type benchmarkFixture struct {
|
|
sessionCount int
|
|
messagesPerSession int
|
|
}
|
|
|
|
func BenchmarkStoreBackends(b *testing.B) {
|
|
ctx := context.Background()
|
|
fixture := benchmarkFixtureFromEnv(b)
|
|
b.Logf(
|
|
"benchmark fixture: %d sessions, %d messages/session, %d total messages",
|
|
fixture.sessionCount,
|
|
fixture.messagesPerSession,
|
|
fixture.sessionCount*fixture.messagesPerSession,
|
|
)
|
|
|
|
stores := setupBenchmarkStores(ctx, b, fixture)
|
|
targetSessionID := sessionID(fixture.sessionCount / 2)
|
|
|
|
benchmarks := []struct {
|
|
name string
|
|
run func(*testing.B, context.Context, db.Store)
|
|
}{
|
|
{
|
|
name: "ListSessions",
|
|
run: func(b *testing.B, ctx context.Context, store db.Store) {
|
|
for range b.N {
|
|
page, err := store.ListSessions(ctx, db.SessionFilter{
|
|
Limit: 50,
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if len(page.Sessions) == 0 {
|
|
b.Fatal("expected sessions")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "SidebarSessionIndex",
|
|
run: func(b *testing.B, ctx context.Context, store db.Store) {
|
|
for range b.N {
|
|
index, err := store.GetSidebarSessionIndex(ctx, db.SessionFilter{
|
|
IncludeChildren: true,
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if len(index.Sessions) == 0 {
|
|
b.Fatal("expected sidebar rows")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Search",
|
|
run: func(b *testing.B, ctx context.Context, store db.Store) {
|
|
for range b.N {
|
|
page, err := store.Search(ctx, db.SearchFilter{
|
|
Query: benchmarkQuery,
|
|
Limit: 25,
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if len(page.Results) == 0 {
|
|
b.Fatal("expected search results")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "GetAllMessages",
|
|
run: func(b *testing.B, ctx context.Context, store db.Store) {
|
|
for range b.N {
|
|
messages, err := store.GetAllMessages(ctx, targetSessionID)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if len(messages) != fixture.messagesPerSession {
|
|
b.Fatalf("expected %d messages, got %d",
|
|
fixture.messagesPerSession, len(messages))
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "AnalyticsSummary",
|
|
run: func(b *testing.B, ctx context.Context, store db.Store) {
|
|
for range b.N {
|
|
summary, err := store.GetAnalyticsSummary(ctx, db.AnalyticsFilter{
|
|
From: "2026-01-01",
|
|
To: "2026-12-31",
|
|
Timezone: "UTC",
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if summary.TotalSessions == 0 {
|
|
b.Fatal("expected analytics sessions")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "DailyUsage",
|
|
run: func(b *testing.B, ctx context.Context, store db.Store) {
|
|
for range b.N {
|
|
usage, err := store.GetDailyUsage(ctx, db.UsageFilter{
|
|
From: "2026-01-01",
|
|
To: "2026-12-31",
|
|
Timezone: "UTC",
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if len(usage.Daily) == 0 {
|
|
b.Fatal("expected usage days")
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, bench := range benchmarks {
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
for _, backend := range stores {
|
|
b.Run(backend.name, func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
bench.run(b, ctx, backend.store)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func benchmarkFixtureFromEnv(b *testing.B) benchmarkFixture {
|
|
b.Helper()
|
|
|
|
return benchmarkFixture{
|
|
sessionCount: positiveIntFromEnv(
|
|
b,
|
|
"AGENTSVIEW_BENCH_SESSIONS",
|
|
defaultBenchmarkSessionCount,
|
|
),
|
|
messagesPerSession: positiveIntFromEnv(
|
|
b,
|
|
"AGENTSVIEW_BENCH_MESSAGES_PER_SESSION",
|
|
defaultBenchmarkMessagesPerSession,
|
|
),
|
|
}
|
|
}
|
|
|
|
func positiveIntFromEnv(b *testing.B, key string, fallback int) int {
|
|
b.Helper()
|
|
|
|
raw := os.Getenv(key)
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
value, err := strconv.Atoi(raw)
|
|
if err != nil || value <= 0 {
|
|
b.Fatalf("%s must be a positive integer, got %q", key, raw)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func setupBenchmarkStores(ctx context.Context, b *testing.B, fixture benchmarkFixture) []benchmarkStore {
|
|
b.Helper()
|
|
|
|
local := openSQLiteStore(b)
|
|
seedBenchmarkFixture(b, local, fixture)
|
|
|
|
duck := openDuckDBStore(ctx, b, local)
|
|
pg := openPostgresStore(ctx, b, local)
|
|
|
|
return []benchmarkStore{
|
|
{name: "sqlite", store: local},
|
|
{name: "duckdb", store: duck},
|
|
{name: "postgres", store: pg},
|
|
}
|
|
}
|
|
|
|
func openSQLiteStore(b *testing.B) *db.DB {
|
|
b.Helper()
|
|
|
|
path := filepath.Join(b.TempDir(), "sessions.db")
|
|
store, err := db.Open(path)
|
|
if err != nil {
|
|
b.Fatalf("open sqlite store: %v", err)
|
|
}
|
|
b.Cleanup(func() {
|
|
if err := store.Close(); err != nil {
|
|
b.Errorf("close sqlite store: %v", err)
|
|
}
|
|
})
|
|
return store
|
|
}
|
|
|
|
func openDuckDBStore(ctx context.Context, b *testing.B, local *db.DB) db.Store {
|
|
b.Helper()
|
|
|
|
syncer, err := duckdb.New(
|
|
filepath.Join(b.TempDir(), "sessions.duckdb"),
|
|
local,
|
|
benchmarkMachine,
|
|
duckdb.SyncOptions{},
|
|
)
|
|
if err != nil {
|
|
b.Fatalf("open duckdb sync: %v", err)
|
|
}
|
|
b.Cleanup(func() {
|
|
if err := syncer.Close(); err != nil {
|
|
b.Errorf("close duckdb sync: %v", err)
|
|
}
|
|
})
|
|
|
|
result, err := syncer.Push(ctx, true, nil)
|
|
if err != nil {
|
|
b.Fatalf("push duckdb fixture: %v", err)
|
|
}
|
|
if result.SessionsPushed == 0 || result.MessagesPushed == 0 {
|
|
b.Fatalf("duckdb fixture push wrote no rows: %+v", result)
|
|
}
|
|
|
|
return duckdb.NewStoreFromDB(syncer.DB())
|
|
}
|
|
|
|
func openPostgresStore(ctx context.Context, b *testing.B, local *db.DB) db.Store {
|
|
b.Helper()
|
|
|
|
container, err := tcpostgres.Run(
|
|
ctx,
|
|
"postgres:17-alpine",
|
|
tcpostgres.WithDatabase("agentsview_bench"),
|
|
tcpostgres.WithUsername("agentsview_bench"),
|
|
tcpostgres.WithPassword("agentsview_bench"),
|
|
testcontainers.WithWaitStrategy(
|
|
wait.ForLog("database system is ready to accept connections").
|
|
WithOccurrence(2).
|
|
WithStartupTimeout(2*time.Minute),
|
|
),
|
|
)
|
|
if err != nil {
|
|
b.Fatalf("start postgres container: %v", err)
|
|
}
|
|
b.Cleanup(func() {
|
|
stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := container.Terminate(
|
|
stopCtx,
|
|
testcontainers.StopTimeout(10*time.Second),
|
|
); err != nil {
|
|
b.Errorf("terminate postgres container: %v", err)
|
|
}
|
|
})
|
|
|
|
pgURL, err := container.ConnectionString(ctx, "sslmode=disable")
|
|
if err != nil {
|
|
b.Fatalf("postgres connection string: %v", err)
|
|
}
|
|
|
|
syncer, err := postgres.New(
|
|
pgURL,
|
|
benchmarkSchema,
|
|
local,
|
|
benchmarkMachine,
|
|
true,
|
|
postgres.SyncOptions{},
|
|
)
|
|
if err != nil {
|
|
b.Fatalf("open postgres sync: %v", err)
|
|
}
|
|
b.Cleanup(func() {
|
|
if err := syncer.Close(); err != nil {
|
|
b.Errorf("close postgres sync: %v", err)
|
|
}
|
|
})
|
|
|
|
result, err := syncer.Push(ctx, true, nil)
|
|
if err != nil {
|
|
b.Fatalf("push postgres fixture: %v", err)
|
|
}
|
|
if result.SessionsPushed == 0 || result.MessagesPushed == 0 {
|
|
b.Fatalf("postgres fixture push wrote no rows: %+v", result)
|
|
}
|
|
|
|
store, err := postgres.NewStore(pgURL, benchmarkSchema, true)
|
|
if err != nil {
|
|
b.Fatalf("open postgres store: %v", err)
|
|
}
|
|
b.Cleanup(func() {
|
|
if err := store.Close(); err != nil {
|
|
b.Errorf("close postgres store: %v", err)
|
|
}
|
|
})
|
|
return store
|
|
}
|
|
|
|
func seedBenchmarkFixture(b *testing.B, store *db.DB, fixture benchmarkFixture) {
|
|
b.Helper()
|
|
|
|
if err := store.UpsertModelPricing([]db.ModelPricing{
|
|
{
|
|
ModelPattern: "claude-bench-*",
|
|
InputPerMTok: 3,
|
|
OutputPerMTok: 15,
|
|
CacheCreationPerMTok: 3.75,
|
|
CacheReadPerMTok: 0.30,
|
|
},
|
|
}); err != nil {
|
|
b.Fatalf("seed pricing: %v", err)
|
|
}
|
|
|
|
start := time.Date(2026, 1, 1, 9, 0, 0, 0, time.UTC)
|
|
for i := range fixture.sessionCount {
|
|
id := sessionID(i)
|
|
project := fmt.Sprintf("project-%02d", i%11)
|
|
agent := "codex"
|
|
if i%3 == 1 {
|
|
agent = "claude"
|
|
} else if i%3 == 2 {
|
|
agent = "cursor"
|
|
}
|
|
startedAt := start.Add(time.Duration(i*37) * time.Minute)
|
|
endedAt := startedAt.Add(time.Duration(fixture.messagesPerSession*3) * time.Minute)
|
|
firstMessage := fmt.Sprintf(
|
|
"Benchmark prompt for %s with %s marker",
|
|
project,
|
|
benchmarkQuery,
|
|
)
|
|
sessionName := fmt.Sprintf("Benchmark %03d", i)
|
|
healthScore := 65 + i%35
|
|
healthGrade := "B"
|
|
if healthScore >= 90 {
|
|
healthGrade = "A"
|
|
} else if healthScore < 75 {
|
|
healthGrade = "C"
|
|
}
|
|
termination := "clean"
|
|
filePath := filepath.Join("/tmp/agentsview-bench", id+".jsonl")
|
|
fileSize := int64(16_384 + i*127)
|
|
fileMtime := endedAt.UnixNano()
|
|
contextPressure := float64(fixture.messagesPerSession*250+i%1000) / 200000
|
|
|
|
session := db.Session{
|
|
ID: id,
|
|
Project: project,
|
|
Machine: benchmarkMachine,
|
|
Agent: agent,
|
|
FirstMessage: &firstMessage,
|
|
SessionName: &sessionName,
|
|
StartedAt: strPtr(startedAt.Format(time.RFC3339)),
|
|
EndedAt: strPtr(endedAt.Format(time.RFC3339)),
|
|
MessageCount: fixture.messagesPerSession,
|
|
UserMessageCount: fixture.messagesPerSession / 2,
|
|
TotalOutputTokens: 4200 + i*9,
|
|
PeakContextTokens: 24000 + i*17,
|
|
HasTotalOutputTokens: true,
|
|
HasPeakContextTokens: true,
|
|
ToolFailureSignalCount: i % 5,
|
|
ToolRetryCount: i % 3,
|
|
EditChurnCount: i % 7,
|
|
ConsecutiveFailureMax: i % 4,
|
|
Outcome: []string{"completed", "blocked", "in_progress"}[i%3],
|
|
OutcomeConfidence: []string{"high", "medium"}[i%2],
|
|
EndedWithRole: []string{"assistant", "user"}[i%2],
|
|
FinalFailureStreak: i % 4,
|
|
CompactionCount: i % 4,
|
|
MidTaskCompactionCount: i % 2,
|
|
ContextPressureMax: &contextPressure,
|
|
HealthScore: &healthScore,
|
|
HealthGrade: &healthGrade,
|
|
HasContextData: true,
|
|
Cwd: filepath.Join("/workspace", project),
|
|
GitBranch: fmt.Sprintf("bench/branch-%02d", i%13),
|
|
SourceVersion: "backendbench",
|
|
TerminationStatus: &termination,
|
|
FilePath: &filePath,
|
|
FileSize: &fileSize,
|
|
FileMtime: &fileMtime,
|
|
CreatedAt: startedAt.Add(-time.Minute).Format(time.RFC3339),
|
|
}
|
|
if err := store.UpsertSession(session); err != nil {
|
|
b.Fatalf("seed session %s: %v", id, err)
|
|
}
|
|
|
|
messages := make([]db.Message, 0, fixture.messagesPerSession)
|
|
for ordinal := range fixture.messagesPerSession {
|
|
timestamp := startedAt.Add(time.Duration(ordinal*3) * time.Minute)
|
|
role := "assistant"
|
|
if ordinal%2 == 0 {
|
|
role = "user"
|
|
}
|
|
content := fmt.Sprintf(
|
|
"%s message %02d for %s in %s with routine benchmark content",
|
|
role,
|
|
ordinal,
|
|
id,
|
|
project,
|
|
)
|
|
if ordinal%11 == 0 {
|
|
content += " " + benchmarkQuery + " query payload"
|
|
}
|
|
contextTokens := 12000 + i*17 + ordinal*211
|
|
outputTokens := 80 + ordinal*7 + i%41
|
|
usage := tokenUsage{
|
|
InputTokens: 900 + ordinal*13,
|
|
OutputTokens: outputTokens,
|
|
CacheCreationInputTokens: 40 + i%11,
|
|
CacheReadInputTokens: 120 + ordinal,
|
|
}
|
|
usageJSON, err := json.Marshal(usage)
|
|
if err != nil {
|
|
b.Fatalf("marshal token usage: %v", err)
|
|
}
|
|
messages = append(messages, db.Message{
|
|
SessionID: id,
|
|
Ordinal: ordinal,
|
|
Role: role,
|
|
Content: content,
|
|
Timestamp: timestamp.Format(time.RFC3339),
|
|
ContentLength: len(content),
|
|
Model: "claude-bench-sonnet",
|
|
TokenUsage: usageJSON,
|
|
ContextTokens: contextTokens,
|
|
OutputTokens: outputTokens,
|
|
HasContextTokens: true,
|
|
HasOutputTokens: true,
|
|
ClaudeMessageID: fmt.Sprintf("%s-%02d", id, ordinal),
|
|
ClaudeRequestID: fmt.Sprintf("request-%03d-%02d", i, ordinal),
|
|
})
|
|
}
|
|
if err := store.InsertMessages(messages); err != nil {
|
|
b.Fatalf("seed messages for %s: %v", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sessionID(i int) string {
|
|
return fmt.Sprintf("bench-session-%04d", i)
|
|
}
|
|
|
|
func strPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
type tokenUsage struct {
|
|
InputTokens int `json:"input_tokens"`
|
|
OutputTokens int `json:"output_tokens"`
|
|
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
|
|
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
|
}
|