Files
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

429 lines
13 KiB
Go

package sync
import (
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/parser"
"go.kenn.io/agentsview/internal/testjsonl"
)
// Hot-path benchmarks for the sync engine, covering the regression
// classes that have shipped before. CI's bench-gate workflow runs
// them on every PR and compares allocs/op, B/op, and ns/op against
// the merge base:
//
// - BenchmarkSyncAllWarmNoop: a full sync over an already-synced,
// unchanged archive must do stat+skip work only. Regressed when
// the provider migration dropped pre-parse DB-freshness skips
// and every full sync reparsed and rewrote unchanged sessions
// (fixed by providerSourceUnchangedInDB), and when discovery
// recomputed root-derived project info per source (#912).
// - BenchmarkSyncPathsIncrementalAppend: absorbing one appended
// line into a large session must scale with the appended data,
// not the stored history (#954).
// - BenchmarkSyncAllColdArchive: first-sync ingest throughput for
// a fresh archive through the default per-session write path.
// - BenchmarkResyncBulkIngest: the same archive through the
// resync bulk-write pipeline (writeBatchBulk /
// DB.WriteSessionBatch, the #411 regression class).
//
// Fixture sizes scale via AGENTSVIEW_BENCH_SYNC_SESSIONS and
// AGENTSVIEW_BENCH_SYNC_MESSAGES for larger local runs.
const (
defaultBenchSyncSessions = 40
defaultBenchSyncMessages = 30
benchLargeSessionLines = 1000
)
// silenceBenchLogs discards the engine's global log output for the
// duration of the benchmark. Log lines interleave with `go test
// -bench` result lines on stdout, which corrupts them so benchfmt
// cannot parse the benchmark — benchgate fails on such corruption,
// and before it did, the corrupted benchmarks silently vanished
// from the gate on both sides.
func silenceBenchLogs(b *testing.B) {
b.Helper()
prev := log.Writer()
log.SetOutput(io.Discard)
b.Cleanup(func() { log.SetOutput(prev) })
}
func benchIntFromEnv(name string, def int) int {
raw := os.Getenv(name)
if raw == "" {
return def
}
n, err := strconv.Atoi(raw)
if err != nil || n <= 0 {
return def
}
return n
}
// writeBenchClaudeArchive lays out `sessions` Claude JSONL session
// files of `perSession` alternating user/assistant messages under
// dir/bench-project, mirroring the on-disk shape SyncAll discovers.
func writeBenchClaudeArchive(
b *testing.B, dir string, sessions, perSession int,
) {
b.Helper()
proj := filepath.Join(dir, "bench-project")
if err := os.MkdirAll(proj, 0o755); err != nil {
b.Fatalf("MkdirAll: %v", err)
}
for s := range sessions {
builder := testjsonl.NewSessionBuilder()
for m := 0; m < perSession; m += 2 {
ts := fmt.Sprintf(
"2026-06-20T10:%02d:%02dZ", (m/2/60)%60, (m/2)%60,
)
builder.AddClaudeUser(ts, fmt.Sprintf(
"user message %d in session %d", m, s,
))
builder.AddClaudeAssistant(ts, fmt.Sprintf(
"assistant reply %d in session %d", m, s,
))
}
path := filepath.Join(
proj, fmt.Sprintf("bench-%04d.jsonl", s),
)
if err := os.WriteFile(
path, []byte(builder.String()), 0o644,
); err != nil {
b.Fatalf("WriteFile %s: %v", path, err)
}
}
}
// openBenchEngine opens a fresh SQLite DB and an engine watching dir
// as a Claude root. Cleanup closes the engine before the DB so any
// pending debounced signal recompute drains first.
func openBenchEngine(b *testing.B, dir string) (*Engine, *db.DB) {
b.Helper()
database, err := db.Open(filepath.Join(b.TempDir(), "bench.db"))
if err != nil {
b.Fatalf("open bench db: %v", err)
}
engine := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentClaude: {dir},
},
Machine: "local",
})
b.Cleanup(func() {
engine.Close()
if err := database.Close(); err != nil {
b.Errorf("close bench db: %v", err)
}
})
return engine, database
}
// BenchmarkSyncAllWarmNoop measures a full sync pass over an archive
// that is already fully synced and unchanged on disk: discovery plus
// per-source freshness skips. It also asserts the invariant the
// benchmark exists to protect — a warm no-op pass must not reparse
// or rewrite anything.
func BenchmarkSyncAllWarmNoop(b *testing.B) {
silenceBenchLogs(b)
sessions := benchIntFromEnv(
"AGENTSVIEW_BENCH_SYNC_SESSIONS", defaultBenchSyncSessions,
)
perSession := benchIntFromEnv(
"AGENTSVIEW_BENCH_SYNC_MESSAGES", defaultBenchSyncMessages,
)
dir := b.TempDir()
writeBenchClaudeArchive(b, dir, sessions, perSession)
engine, _ := openBenchEngine(b, dir)
ctx := context.Background()
first := engine.SyncAll(ctx, nil)
if first.Synced != sessions {
b.Fatalf(
"initial sync stored %d of %d sessions (failed=%d)",
first.Synced, sessions, first.Failed,
)
}
b.ReportAllocs()
b.ResetTimer()
for range b.N {
stats := engine.SyncAll(ctx, nil)
if stats.Synced != 0 {
b.Fatalf(
"warm no-op sync re-synced %d sessions", stats.Synced,
)
}
if writes := engine.PhaseStats().BatchedWrites.Load(); writes != 0 {
b.Fatalf(
"warm no-op sync bulk-wrote %d sessions", writes,
)
}
}
}
// BenchmarkSyncPathsIncrementalAppend measures absorbing a single
// appended JSONL line into a session that already stores
// benchLargeSessionLines messages, the streaming write the serve
// daemon performs thousands of times per day.
//
// The session grows by one message per iteration, so per-op cost is
// only comparable between runs with the same iteration count: the
// bench gate always runs with a fixed -benchtime=Nx (see bench.yml
// and the Makefile) so baseline and candidate absorb appends into
// identically sized sessions. Growth is deliberate — per-append cost
// staying flat as the session grows is exactly the invariant this
// benchmark protects.
func BenchmarkSyncPathsIncrementalAppend(b *testing.B) {
silenceBenchLogs(b)
dir := b.TempDir()
writeBenchClaudeArchive(b, dir, 1, benchLargeSessionLines)
engine, database := openBenchEngine(b, dir)
ctx := context.Background()
first := engine.SyncAll(ctx, nil)
if first.Synced != 1 {
b.Fatalf(
"initial sync stored %d sessions (failed=%d)",
first.Synced, first.Failed,
)
}
path := filepath.Join(dir, "bench-project", "bench-0000.jsonl")
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
b.Fatalf("OpenFile: %v", err)
}
defer f.Close()
// The first append after a full sync triggers the leading-edge
// inline signal recompute — an O(stored history) message reload
// plus secret scan whose one-time cost would otherwise dominate
// the fixed 20-iteration measurement and mask steady-state
// per-append regressions. Absorb it before the timer starts so
// the timed loop measures the debounced steady state.
warmup := testjsonl.NewSessionBuilder().AddClaudeUser(
"2026-06-20T11:00:00Z", "warm-up append",
).String()
if _, err := f.WriteString(warmup); err != nil {
b.Fatalf("warm-up append: %v", err)
}
engine.SyncPaths([]string{path})
// Stretch the debounce window so the flush timer cannot fire
// inside the timed loop on a slow run: allocs/op is gated on the
// candidate's worst -count run, and one timer-driven O(history)
// recompute landing mid-loop would flake the gate. Engine.Close
// drains the deferred recompute during cleanup.
engine.signalSched.mu.Lock()
engine.signalSched.interval = time.Hour
engine.signalSched.quiet = time.Hour
engine.signalSched.mu.Unlock()
// Pre-build the appended lines: constructing Claude JSONL via
// testjsonl allocates (map + json.Marshal), and inside the timed
// loop that helper cost would be gated as if it were sync work.
lines := make([]string, b.N)
for i := range lines {
lines[i] = testjsonl.NewSessionBuilder().AddClaudeUser(
"2026-06-20T11:00:00Z",
fmt.Sprintf("streamed line %d", i),
).String()
}
b.ReportAllocs()
b.ResetTimer()
for i := range b.N {
if _, err := f.WriteString(lines[i]); err != nil {
b.Fatalf("append: %v", err)
}
engine.SyncPaths([]string{path})
}
b.StopTimer()
msgs, err := database.GetAllMessages(ctx, "bench-0000")
if err != nil {
b.Fatalf("GetAllMessages: %v", err)
}
want := benchLargeSessionLines + 1 + b.N // +1 for the warm-up append
if len(msgs) < want {
b.Fatalf(
"appends were not absorbed: stored %d messages, want >= %d",
len(msgs), want,
)
}
}
// benchColdArchive is the shared cold-ingest loop: each iteration
// syncs the same archive into a fresh database via syncOnce, then
// verify checks the iteration's outcome with the timer stopped.
func benchColdArchive(
b *testing.B,
syncOnce func(*Engine) SyncStats,
verify func(*Engine, SyncStats, int),
) {
b.Helper()
silenceBenchLogs(b)
sessions := benchIntFromEnv(
"AGENTSVIEW_BENCH_SYNC_SESSIONS", defaultBenchSyncSessions,
)
perSession := benchIntFromEnv(
"AGENTSVIEW_BENCH_SYNC_MESSAGES", defaultBenchSyncMessages,
)
dir := b.TempDir()
writeBenchClaudeArchive(b, dir, sessions, perSession)
dbDir := b.TempDir()
b.ReportAllocs()
b.ResetTimer()
for i := range b.N {
b.StopTimer()
dbPath := filepath.Join(dbDir, fmt.Sprintf("cold-%d.db", i))
database, err := db.Open(dbPath)
if err != nil {
b.Fatalf("open bench db: %v", err)
}
engine := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentClaude: {dir},
},
Machine: "local",
})
b.StartTimer()
stats := syncOnce(engine)
b.StopTimer()
verify(engine, stats, sessions)
engine.Close()
if err := database.Close(); err != nil {
b.Fatalf("close bench db: %v", err)
}
// Drop this iteration's DB (and WAL/SHM sidecars) so disk
// usage stays O(1) instead of retaining b.N populated
// databases until the function returns.
stale, err := filepath.Glob(dbPath + "*")
if err != nil {
b.Fatalf("glob %s: %v", dbPath, err)
}
for _, p := range stale {
if err := os.Remove(p); err != nil {
b.Fatalf("remove %s: %v", p, err)
}
}
b.StartTimer()
}
}
// BenchmarkSyncAllColdArchive measures first-sync ingest throughput
// through the public SyncAll path: parse plus the default
// per-session writes a user's first sync performs.
func BenchmarkSyncAllColdArchive(b *testing.B) {
ctx := context.Background()
benchColdArchive(b,
func(engine *Engine) SyncStats {
return engine.SyncAll(ctx, nil)
},
func(_ *Engine, stats SyncStats, sessions int) {
if stats.Synced != sessions {
b.Fatalf(
"cold sync stored %d of %d sessions (failed=%d)",
stats.Synced, sessions, stats.Failed,
)
}
},
)
}
// BenchmarkResyncBulkIngest measures the bulk-write ingest path a
// full resync uses: syncWriteBulk routes every parsed session
// through writeBatchBulk and DB.WriteSessionBatch — the #411
// regression class. This is a different write path from the default
// per-session writes BenchmarkSyncAllColdArchive covers, and it
// self-asserts that every session really went through the batch
// pipeline so the benchmark cannot silently measure the wrong path.
func BenchmarkResyncBulkIngest(b *testing.B) {
ctx := context.Background()
benchColdArchive(b,
func(engine *Engine) SyncStats {
engine.syncMu.Lock()
defer engine.syncMu.Unlock()
return engine.syncAllLocked(
ctx, nil, time.Time{}, nil, syncWriteBulk, true, false,
)
},
func(engine *Engine, stats SyncStats, sessions int) {
if stats.Synced != sessions {
b.Fatalf(
"bulk ingest stored %d of %d sessions (failed=%d)",
stats.Synced, sessions, stats.Failed,
)
}
writes := engine.PhaseStats().BatchedWrites.Load()
if writes != int64(sessions) {
b.Fatalf(
"bulk ingest wrote %d of %d sessions via the batch pipeline",
writes, sessions,
)
}
},
)
}
// BenchmarkResyncBulkContributorIngest measures the same cold archive shape
// when it enters the atomic rebuild through a contributor engine.
func BenchmarkResyncBulkContributorIngest(b *testing.B) {
ctx := context.Background()
benchColdArchive(b,
func(engine *Engine) SyncStats {
dir := engine.agentDirs[parser.AgentClaude][0]
engine.agentDirs = nil
stats, err := engine.ResyncAllWithOptions(ctx, nil, RebuildOptions{
Contributors: []RebuildContributor{{
Name: "benchmark-contributor",
Config: EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentClaude: {dir},
},
Machine: "benchmark-contributor",
IDPrefix: "benchmark~",
Ephemeral: true,
},
}},
})
if err != nil {
b.Fatalf("contributor rebuild: %v", err)
}
return stats
},
func(_ *Engine, stats SyncStats, sessions int) {
if stats.Synced != sessions {
b.Fatalf(
"contributor ingest stored %d of %d sessions (failed=%d)",
stats.Synced, sessions, stats.Failed,
)
}
if len(stats.RebuildPhases) != 2 {
b.Fatalf("contributor ingest recorded %d rebuild phases", len(stats.RebuildPhases))
}
writes := stats.RebuildPhases[1].BatchedWrites
if writes != int64(sessions) {
b.Fatalf(
"contributor ingest wrote %d of %d sessions via the batch pipeline",
writes, sessions,
)
}
},
)
}