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
267 lines
6.8 KiB
Go
267 lines
6.8 KiB
Go
package analysis
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/query"
|
|
)
|
|
|
|
// buildSyntheticGraph creates a graph with the given number of function nodes
|
|
// and approximately 2*nodeCount random-ish call edges for benchmarking.
|
|
func buildSyntheticGraph(b *testing.B, nodeCount int) *graph.Graph {
|
|
b.Helper()
|
|
g := graph.New()
|
|
for i := 0; i < nodeCount; i++ {
|
|
g.AddNode(&graph.Node{
|
|
ID: fmt.Sprintf("pkg/file%d.go::Func%d", i/100, i),
|
|
Kind: graph.KindFunction,
|
|
Name: fmt.Sprintf("Func%d", i),
|
|
FilePath: fmt.Sprintf("pkg/file%d.go", i/100),
|
|
StartLine: (i % 100) + 1,
|
|
EndLine: (i % 100) + 10,
|
|
Language: "go",
|
|
})
|
|
}
|
|
// Add ~2x edges with a deterministic pattern
|
|
for i := 0; i < nodeCount; i++ {
|
|
// Each node calls the next 2 nodes (wrapping)
|
|
t1 := (i + 1) % nodeCount
|
|
t2 := (i + 7) % nodeCount
|
|
g.AddEdge(&graph.Edge{
|
|
From: fmt.Sprintf("pkg/file%d.go::Func%d", i/100, i),
|
|
To: fmt.Sprintf("pkg/file%d.go::Func%d", t1/100, t1),
|
|
Kind: graph.EdgeCalls,
|
|
})
|
|
g.AddEdge(&graph.Edge{
|
|
From: fmt.Sprintf("pkg/file%d.go::Func%d", i/100, i),
|
|
To: fmt.Sprintf("pkg/file%d.go::Func%d", t2/100, t2),
|
|
Kind: graph.EdgeCalls,
|
|
})
|
|
}
|
|
return g
|
|
}
|
|
|
|
// buildCommunities creates a simple CommunityResult assigning nodes to communities
|
|
// based on their file grouping (every 100 nodes = 1 community).
|
|
func buildCommunities(g *graph.Graph) *CommunityResult {
|
|
nodes := g.AllNodes()
|
|
result := &CommunityResult{
|
|
NodeToComm: make(map[string]string),
|
|
}
|
|
commMap := make(map[string][]string)
|
|
for _, n := range nodes {
|
|
commID := fmt.Sprintf("community-%s", n.FilePath)
|
|
result.NodeToComm[n.ID] = commID
|
|
commMap[commID] = append(commMap[commID], n.ID)
|
|
}
|
|
for id, members := range commMap {
|
|
result.Communities = append(result.Communities, Community{
|
|
ID: id,
|
|
Label: id,
|
|
Members: members,
|
|
Size: len(members),
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// --- BenchmarkDetectCycles ---
|
|
|
|
func BenchmarkDetectCycles_1k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 1000)
|
|
comms := buildCommunities(g)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
DetectCycles(g, comms, "")
|
|
}
|
|
}
|
|
|
|
func BenchmarkDetectCycles_10k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 10000)
|
|
comms := buildCommunities(g)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
DetectCycles(g, comms, "")
|
|
}
|
|
}
|
|
|
|
func BenchmarkDetectCycles_100k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 100000)
|
|
comms := buildCommunities(g)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
DetectCycles(g, comms, "")
|
|
}
|
|
}
|
|
|
|
// --- BenchmarkFindDeadCode ---
|
|
|
|
func BenchmarkFindDeadCode_1k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 1000)
|
|
procs := &ProcessResult{NodeToProcs: make(map[string][]string)}
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
FindDeadCode(g, procs, nil)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFindDeadCode_10k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 10000)
|
|
procs := &ProcessResult{NodeToProcs: make(map[string][]string)}
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
FindDeadCode(g, procs, nil)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFindDeadCode_100k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 100000)
|
|
procs := &ProcessResult{NodeToProcs: make(map[string][]string)}
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
FindDeadCode(g, procs, nil)
|
|
}
|
|
}
|
|
|
|
// --- BenchmarkFindHotspots ---
|
|
|
|
func BenchmarkFindHotspots_1k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 1000)
|
|
comms := buildCommunities(g)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
FindHotspots(g, comms, 0)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFindHotspots_10k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 10000)
|
|
comms := buildCommunities(g)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
FindHotspots(g, comms, 0)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFindHotspots_100k(b *testing.B) {
|
|
g := buildSyntheticGraph(b, 100000)
|
|
comms := buildCommunities(g)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
FindHotspots(g, comms, 0)
|
|
}
|
|
}
|
|
|
|
// --- BenchmarkVerifyChanges ---
|
|
|
|
// buildGraphWithCallers creates a graph with one target function and N callers.
|
|
func buildGraphWithCallers(b *testing.B, callerCount int) (*graph.Graph, *query.Engine) {
|
|
b.Helper()
|
|
g := graph.New()
|
|
targetID := "pkg/target.go::Target"
|
|
g.AddNode(&graph.Node{
|
|
ID: targetID,
|
|
Kind: graph.KindFunction,
|
|
Name: "Target",
|
|
FilePath: "pkg/target.go",
|
|
StartLine: 1,
|
|
EndLine: 10,
|
|
Language: "go",
|
|
Meta: map[string]any{"signature": "func(ctx context.Context, id string) error"},
|
|
})
|
|
for i := 0; i < callerCount; i++ {
|
|
callerID := fmt.Sprintf("pkg/caller%d.go::Caller%d", i, i)
|
|
g.AddNode(&graph.Node{
|
|
ID: callerID,
|
|
Kind: graph.KindFunction,
|
|
Name: fmt.Sprintf("Caller%d", i),
|
|
FilePath: fmt.Sprintf("pkg/caller%d.go", i),
|
|
StartLine: 1,
|
|
EndLine: 5,
|
|
Language: "go",
|
|
})
|
|
g.AddEdge(&graph.Edge{
|
|
From: callerID,
|
|
To: targetID,
|
|
Kind: graph.EdgeCalls,
|
|
})
|
|
}
|
|
eng := query.NewEngine(g)
|
|
return g, eng
|
|
}
|
|
|
|
func BenchmarkVerifyChanges_10callers(b *testing.B) {
|
|
g, eng := buildGraphWithCallers(b, 10)
|
|
changes := []SignatureChange{{SymbolID: "pkg/target.go::Target", NewSignature: "func(ctx context.Context, id string, extra int) error"}}
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
VerifyChanges(g, eng, changes)
|
|
}
|
|
}
|
|
|
|
func BenchmarkVerifyChanges_100callers(b *testing.B) {
|
|
g, eng := buildGraphWithCallers(b, 100)
|
|
changes := []SignatureChange{{SymbolID: "pkg/target.go::Target", NewSignature: "func(ctx context.Context, id string, extra int) error"}}
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
VerifyChanges(g, eng, changes)
|
|
}
|
|
}
|
|
|
|
func BenchmarkVerifyChanges_1000callers(b *testing.B) {
|
|
g, eng := buildGraphWithCallers(b, 1000)
|
|
changes := []SignatureChange{{SymbolID: "pkg/target.go::Target", NewSignature: "func(ctx context.Context, id string, extra int) error"}}
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
VerifyChanges(g, eng, changes)
|
|
}
|
|
}
|
|
|
|
// --- BenchmarkEnsureFresh ---
|
|
// ensureFresh is in the mcp package, so we benchmark the IsStale check pattern
|
|
// which is the core of the ensureFresh hot path.
|
|
|
|
func BenchmarkIsStaleCheck_100files(b *testing.B) {
|
|
benchmarkIsStalePattern(b, 100)
|
|
}
|
|
|
|
func BenchmarkIsStaleCheck_1000files(b *testing.B) {
|
|
benchmarkIsStalePattern(b, 1000)
|
|
}
|
|
|
|
func BenchmarkIsStaleCheck_10000files(b *testing.B) {
|
|
benchmarkIsStalePattern(b, 10000)
|
|
}
|
|
|
|
// benchmarkIsStalePattern simulates the ensureFresh file-staleness check loop.
|
|
// It builds a map of file mtimes and iterates over N files checking staleness.
|
|
func benchmarkIsStalePattern(b *testing.B, fileCount int) {
|
|
b.Helper()
|
|
// Build a mtime map simulating the indexer's fileMtimes
|
|
mtimes := make(map[string]int64, fileCount)
|
|
files := make([]string, fileCount)
|
|
for i := 0; i < fileCount; i++ {
|
|
fp := fmt.Sprintf("pkg/dir%d/file%d.go", i/100, i)
|
|
files[i] = fp
|
|
mtimes[fp] = int64(i * 1000000) // fake mtime
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
// Simulate ensureFresh: check up to 5 stale files
|
|
limit := 5
|
|
refreshed := 0
|
|
for _, fp := range files {
|
|
if refreshed >= limit {
|
|
break
|
|
}
|
|
// Simulate IsStale check: lookup in map
|
|
if _, ok := mtimes[fp]; ok {
|
|
refreshed++
|
|
}
|
|
}
|
|
}
|
|
}
|