Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

116 lines
3.7 KiB
Go

package indexer
import (
"os"
"path/filepath"
"runtime"
"testing"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph/store_sqlite"
"github.com/zzet/gortex/internal/parser"
"github.com/zzet/gortex/internal/parser/languages"
)
// BenchmarkEditFileReindex measures the synchronous work an edit_file
// request pays after the text splice: reindexFile -> IndexFile ->
// indexFile(resolve=true). Because indexFile does NO change-diffing,
// re-indexing an unchanged file does the exact same work as a real edit,
// so we can measure the true hot path without writing to the repo.
//
// Corpus defaults to the repo root (../.. from internal/indexer) so the
// graph matches production scale; override with GORTEX_BENCH_CORPUS (a
// smaller subtree, e.g. internal/graph, runs far faster for before/after
// comparison since the FTS-delete cost scales with corpus size). Target
// file defaults to the sqlite store.go; override with GORTEX_BENCH_TARGET
// (path relative to the corpus root).
//
// Run (bypass rtk — it eats benchmark output):
//
// rtk proxy go test ./internal/indexer -run '^$' -bench BenchmarkEditFileReindex \
// -benchtime=20x -cpuprofile=/tmp/edit.cpu
// go tool pprof -top -nodecount=60 -cum /tmp/edit.cpu
func BenchmarkEditFileReindex(b *testing.B) {
corpus := os.Getenv("GORTEX_BENCH_CORPUS")
if corpus == "" {
corpus = "../.." // repo root, relative to internal/indexer
}
corpus, err := filepath.Abs(corpus)
if err != nil {
b.Fatalf("abs corpus: %v", err)
}
if _, err := os.Stat(corpus); err != nil {
b.Skipf("corpus %s not available: %v", corpus, err)
}
targetRel := os.Getenv("GORTEX_BENCH_TARGET")
if targetRel == "" {
targetRel = "internal/graph/store_sqlite/store.go"
}
target := filepath.Join(corpus, targetRel)
if _, err := os.Stat(target); err != nil {
b.Skipf("target %s not available: %v", target, err)
}
store, err := store_sqlite.Open(filepath.Join(b.TempDir(), "bench.sqlite"))
if err != nil {
b.Fatalf("open sqlite: %v", err)
}
defer func() { _ = store.Close() }()
reg := parser.NewRegistry()
languages.RegisterAll(reg)
cfg := config.Default().Index
cfg.Workers = runtime.NumCPU()
idx := New(store, reg, cfg, zap.NewNop())
// One-time bulk index (NOT timed). This is what the daemon already
// did; we are measuring the per-edit cost on top of a warm graph.
if _, err := idx.Index(corpus); err != nil {
b.Fatalf("bulk index: %v", err)
}
st := store.Stats()
b.Logf("graph: %d nodes, %d edges (corpus=%s, target=%s)",
st.TotalNodes, st.TotalEdges, corpus, targetRel)
// graphPath is the key indexFile derives for the reverse/resolver
// passes — relKey + repo prefix, exactly as indexFile computes it.
abs, _ := filepath.Abs(target)
graphPath := idx.prefixPath(idx.relKey(abs))
// 1. Full hot path: parse + evict + add + persist + resolve. This is
// what edit_file pays today.
b.Run("IndexFile_resolve", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if err := idx.IndexFile(target); err != nil {
b.Fatalf("IndexFile: %v", err)
}
}
})
// 2. Same minus the per-file resolver call — isolates the parse +
// EvictFile + AddBatch + persist (sidecar) cost, incl. the FTS
// upsert loop.
b.Run("IndexFile_noresolve", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if err := idx.IndexFileNoResolve(target); err != nil {
b.Fatalf("IndexFileNoResolve: %v", err)
}
}
})
// 3. The resolver pass alone — buildPassIndexes (4 whole-graph index
// builds) + forward + reverse resolve. (1)-(2) ~= this.
b.Run("ResolveFileAndIncoming", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
idx.resolver.ResolveFileAndIncoming(graphPath)
}
})
}