feat(cli): add 'bd thanks' command to thank contributors

Adds a new command that displays a thank you page listing all human
contributors to the beads project. Features:

- Static list of contributors (compiled into binary)
- Top 20 featured contributors displayed in columns
- Additional contributors in wrapped list
- Styled output using lipgloss (colored box, sections)
- Dynamic width based on content
- JSON output support (--json flag)
- Excludes bots and AI agents by email pattern
This commit is contained in:
Ryan Snodgrass
2025-12-14 12:38:19 -08:00
parent 3a9749279a
commit f88a0d015b
7 changed files with 1363 additions and 794 deletions

File diff suppressed because one or more lines are too long

140
BENCHMARKS.md Normal file
View File

@@ -0,0 +1,140 @@
# Beads Performance Benchmarks
This document describes the performance benchmarks available in the beads project and how to use them.
## Running Benchmarks
### All SQLite Benchmarks
```bash
go test -tags=bench -bench=. -benchmem ./internal/storage/sqlite/...
```
### Specific Benchmark
```bash
go test -tags=bench -bench=BenchmarkGetReadyWork_Large -benchmem ./internal/storage/sqlite/...
```
### With CPU Profiling
```bash
go test -tags=bench -bench=BenchmarkGetReadyWork_Large -cpuprofile=cpu.prof ./internal/storage/sqlite/...
go tool pprof -http=:8080 cpu.prof
```
## Benchmark Categories
### Compaction Operations
- **BenchmarkGetTier1Candidates** - Identify L1 compaction candidates
- **BenchmarkGetTier2Candidates** - Identify L2 compaction candidates
- **BenchmarkCheckEligibility** - Check if issue is eligible for compaction
### Cycle Detection
Tests on graphs with different topologies (linear chains, trees, dense graphs):
- **BenchmarkCycleDetection_Linear_100/1000/5000** - Linear dependency chains
- **BenchmarkCycleDetection_Tree_100/1000** - Tree-structured dependencies
- **BenchmarkCycleDetection_Dense_100/1000** - Dense graphs
### Ready Work / Filtering
- **BenchmarkGetReadyWork_Large** - Filter unblocked issues (10K dataset)
- **BenchmarkGetReadyWork_XLarge** - Filter unblocked issues (20K dataset)
- **BenchmarkGetReadyWork_FromJSONL** - Ready work on JSONL-imported database
### Search Operations
- **BenchmarkSearchIssues_Large_NoFilter** - Search all open issues (10K dataset)
- **BenchmarkSearchIssues_Large_ComplexFilter** - Search with priority/status filters (10K dataset)
### CRUD Operations
- **BenchmarkCreateIssue_Large** - Create new issue in 10K database
- **BenchmarkUpdateIssue_Large** - Update existing issue in 10K database
- **BenchmarkBulkCloseIssues** - Close 100 issues sequentially (NEW)
### Specialized Operations
- **BenchmarkLargeDescription** - Handling 100KB+ issue descriptions (NEW)
- **BenchmarkSyncMerge** - Simulate sync cycle with create/update operations (NEW)
## Performance Targets
### Typical Results (M2 Pro)
| Operation | Time | Memory | Notes |
|-----------|------|--------|-------|
| GetReadyWork (10K) | 30ms | 16.8MB | Filters ~200 open issues |
| Search (10K, no filter) | 12.5ms | 6.3MB | Returns all open issues |
| Cycle Detection (5000 linear) | 70ms | 15KB | Detects transitive deps |
| Create Issue (10K db) | 2.5ms | 8.9KB | Insert into index |
| Update Issue (10K db) | 18ms | 17KB | Status change |
| **Large Description (100KB)** | **3.3ms** | **874KB** | String handling overhead |
| **Bulk Close (100 issues)** | **1.9s** | **1.2MB** | 100 sequential writes |
| **Sync Merge (20 ops)** | **29ms** | **198KB** | Create 10 + update 10 |
## Dataset Caching
Benchmark datasets are cached in `/tmp/beads-bench-cache/`:
- `large.db` - 10,000 issues (16.6 MB)
- `xlarge.db` - 20,000 issues (generated on demand)
- `large-jsonl.db` - 10K issues via JSONL import
Cached databases are reused across runs. To regenerate:
```bash
rm /tmp/beads-bench-cache/*.db
```
## Adding New Benchmarks
Follow the pattern in `sqlite_bench_test.go`:
```go
// BenchmarkMyTest benchmarks a specific operation
func BenchmarkMyTest(b *testing.B) {
runBenchmark(b, setupLargeBenchDB, func(store *SQLiteStorage, ctx context.Context) error {
// Your test code here
return err
})
}
```
Or for custom setup:
```go
func BenchmarkMyTest(b *testing.B) {
store, cleanup := setupLargeBenchDB(b)
defer cleanup()
ctx := context.Background()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Your test code here
}
}
```
## CPU Profiling
The benchmark suite automatically enables CPU profiling on the first benchmark run:
```
CPU profiling enabled: bench-cpu-2025-12-07-174417.prof
View flamegraph: go tool pprof -http=:8080 bench-cpu-2025-12-07-174417.prof
```
This generates a flamegraph showing where time is spent across all benchmarks.
## Performance Optimization Strategy
1. **Identify bottleneck** - Run benchmarks to find slow operations
2. **Profile** - Use CPU profiling to see which functions consume time
3. **Measure** - Run baseline benchmark before optimization
4. **Optimize** - Make targeted changes
5. **Verify** - Re-run benchmark to measure improvement
Example:
```bash
# Baseline
go test -tags=bench -bench=BenchmarkGetReadyWork_Large -benchmem ./internal/storage/sqlite/...
# Make changes...
# Measure improvement
go test -tags=bench -bench=BenchmarkGetReadyWork_Large -benchmem ./internal/storage/sqlite/...
```

View File

@@ -183,7 +183,18 @@ func acquireStartLock(lockPath, socketPath string) bool {
// nolint:gosec // G304: lockPath is derived from secure beads directory
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
if err != nil {
debugLog("another process is starting daemon, waiting for readiness")
// Lock file exists - check if it's from a dead process (stale) or alive daemon
lockPID, pidErr := readPIDFromFile(lockPath)
if pidErr != nil || !isPIDAlive(lockPID) {
// Stale lock from crashed process - clean up immediately (avoids 5s wait)
debugLog("startlock is stale (PID %d dead or unreadable), cleaning up", lockPID)
_ = os.Remove(lockPath)
// Retry lock acquisition after cleanup
return acquireStartLock(lockPath, socketPath)
}
// PID is alive - daemon is legitimately starting, wait for socket to be ready
debugLog("another process (PID %d) is starting daemon, waiting for readiness", lockPID)
if waitForSocketReadiness(socketPath, 5*time.Second) {
return true
}

269
cmd/bd/thanks.go Normal file
View File

@@ -0,0 +1,269 @@
package main
import (
"fmt"
"sort"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)
// lipgloss styles for the thanks page
var (
thanksTitleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("7")) // white, bold
thanksSubtitleStyle = lipgloss.NewStyle().
Faint(true)
thanksSectionStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("6")). // cyan for section headers
Bold(true)
thanksNameStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("2")) // green
thanksLabelStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("3")) // yellow
thanksDimStyle = lipgloss.NewStyle().
Faint(true)
)
// thanksBoxStyle returns a box style with dynamic width
func thanksBoxStyle(width int) lipgloss.Style {
return lipgloss.NewStyle().
BorderStyle(lipgloss.DoubleBorder()).
BorderForeground(lipgloss.Color("7")).
Padding(1, 4).
Width(width - 4). // account for border
Align(lipgloss.Center)
}
// Static list of human contributors to the beads project.
// To update: run `git shortlog -sn --all` in the beads repo.
// Map of contributor name -> commit count, sorted by contribution count descending.
var beadsContributors = map[string]int{
"Steve Yegge": 2959,
"matt wilkie": 64,
"Ryan Snodgrass": 43,
"Travis Cline": 9,
"David Laing": 7,
"Ryan Newton": 6,
"Joshua Shanks": 6,
"Daan van Etten": 5,
"Augustinas Malinauskas": 4,
"Matteo Landi": 4,
"Baishampayan Ghose": 4,
"Charles P. Cross": 4,
"Abhinav Gupta": 3,
"Brian Williams": 3,
"Marco Del Pin": 3,
"Willi Ballenthin": 3,
"Ben Lovell": 2,
"Ben Madore": 2,
"Dane Bertram": 2,
"Dennis Schön": 2,
"Troy Gaines": 2,
"Zoe Gagnon": 2,
"Peter Schilling": 2,
"Adam Spiers": 1,
"Aodhan Hayter": 1,
"Assim Elhammouti": 1,
"Bryce Roche": 1,
"Caleb Leak": 1,
"David Birks": 1,
"Dean Giberson": 1,
"Eli": 1,
"Graeme Foster": 1,
"Gurdas Nijor": 1,
"Jimmy Stridh": 1,
"Joel Klabo": 1,
"Johannes Zillmann": 1,
"John Lam": 1,
"Jonathan Berger": 1,
"Joshua Park": 1,
"Juan Vargas": 1,
"Kasper Zutterman": 1,
"Kris Hansen": 1,
"Logan Thomas": 1,
"Lon Lundgren": 1,
"Mark Wotton": 1,
"Markus Flür": 1,
"Michael Shuffett": 1,
"Midworld Kim": 1,
"Nikolai Prokoschenko": 1,
"Peter Loron": 1,
"Rod Davenport": 1,
"Serhii": 1,
"Shaun Cutts": 1,
"Sophie Smithburg": 1,
"Tim Haasdyk": 1,
"Travis Lyons": 1,
"Yaakov Nemoy": 1,
"Yunsik Kim": 1,
"Zachary Rosen": 1,
}
var thanksCmd = &cobra.Command{
Use: "thanks",
Short: "Thank the human contributors to beads",
Long: `Display a thank you page listing all human contributors to the beads project.
Examples:
bd thanks # Show thank you page
`,
Run: func(cmd *cobra.Command, args []string) {
printThanksPage()
},
}
// getContributorsSorted returns contributors sorted by commit count descending
func getContributorsSorted() []string {
type kv struct {
name string
commits int
}
var sorted []kv
for name, commits := range beadsContributors {
sorted = append(sorted, kv{name, commits})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].commits > sorted[j].commits
})
names := make([]string, len(sorted))
for i, kv := range sorted {
names[i] = kv.name
}
return names
}
// printThanksPage displays the thank you page
func printThanksPage() {
fmt.Println()
// get sorted contributors and split into top 20 and rest
allContributors := getContributorsSorted()
topN := 20
if topN > len(allContributors) {
topN = len(allContributors)
}
topContributors := allContributors[:topN]
additionalContributors := allContributors[topN:]
// calculate content width based on featured contributors columns
contentWidth := calculateColumnsWidth(topContributors, 4) + 4 // +4 for indent
// build header content with styled text
title := thanksTitleStyle.Render("THANK YOU!")
subtitle := thanksSubtitleStyle.Render("To all the humans who contributed to beads")
header := title + "\n\n" + subtitle
// render header in a bordered box matching content width
fmt.Println(thanksBoxStyle(contentWidth).Render(header))
fmt.Println()
// print featured contributors section
fmt.Println(thanksSectionStyle.Render(" Featured Contributors"))
fmt.Println()
printThanksColumns(topContributors, 4)
// print additional contributors with line wrapping
if len(additionalContributors) > 0 {
fmt.Println()
fmt.Println(thanksSectionStyle.Render(" Additional Contributors"))
fmt.Println()
printThanksWrappedList("", additionalContributors, contentWidth)
}
fmt.Println()
}
// calculateColumnsWidth returns the total width needed for displaying names in columns
func calculateColumnsWidth(names []string, cols int) int {
if len(names) == 0 {
return 0
}
maxWidth := 0
for _, name := range names {
if len(name) > maxWidth {
maxWidth = len(name)
}
}
if maxWidth > 20 {
maxWidth = 20
}
colWidth := maxWidth + 2
return colWidth * cols
}
// printThanksColumns prints names in n columns, sorted horizontally by input order
func printThanksColumns(names []string, cols int) {
if len(names) == 0 {
return
}
// find max width for alignment
maxWidth := 0
for _, name := range names {
if len(name) > maxWidth {
maxWidth = len(name)
}
}
if maxWidth > 20 {
maxWidth = 20
}
colWidth := maxWidth + 2
// print in rows, reading left to right
for i := 0; i < len(names); i += cols {
fmt.Print(" ")
for j := 0; j < cols && i+j < len(names); j++ {
name := names[i+j]
if len(name) > 20 {
name = name[:17] + "..."
}
padded := fmt.Sprintf("%-*s", colWidth, name)
fmt.Print(thanksNameStyle.Render(padded))
}
fmt.Println()
}
}
// printThanksWrappedList prints a list with word wrapping at name boundaries
func printThanksWrappedList(label string, names []string, maxWidth int) {
indent := " "
fmt.Print(indent)
lineLen := len(indent)
if label != "" {
fmt.Print(thanksLabelStyle.Render(label) + " ")
lineLen += len(label) + 1
}
for i, name := range names {
suffix := ", "
if i == len(names)-1 {
suffix = ""
}
entry := name + suffix
if lineLen+len(entry) > maxWidth && lineLen > len(indent) {
fmt.Println()
fmt.Print(indent)
lineLen = len(indent)
}
fmt.Print(thanksDimStyle.Render(entry))
lineLen += len(entry)
}
fmt.Println()
}
func init() {
rootCmd.AddCommand(thanksCmd)
}

11
go.mod
View File

@@ -22,12 +22,22 @@ require (
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
@@ -38,6 +48,7 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect

23
go.sum
View File

@@ -1,5 +1,17 @@
github.com/anthropics/anthropic-sdk-go v1.18.1 h1:HZ7/kW/V2GN1N86rQKNW28/wfvLv9IR6bPEqBTn9eR0=
github.com/anthropics/anthropic-sdk-go v1.18.1/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -19,11 +31,17 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-sqlite3 v0.30.1 h1:pHC3YsyRdJv4pCMB4MO1Q2BXw/CAa+Hoj7GSaKtVk+g=
github.com/ncruces/go-sqlite3 v0.30.1/go.mod h1:UVsWrQaq1qkcal5/vT5lOJnZCVlR5rsThKdwidjFsKc=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
@@ -32,6 +50,9 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -66,6 +87,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=

View File

@@ -139,6 +139,103 @@ func BenchmarkGetReadyWork_FromJSONL(b *testing.B) {
})
}
// BenchmarkLargeDescription benchmarks handling of issues with very large descriptions (100KB+)
func BenchmarkLargeDescription(b *testing.B) {
runBenchmark(b, setupLargeBenchDB, func(store *SQLiteStorage, ctx context.Context) error {
// Create issue with 100KB description
largeDesc := make([]byte, 100*1024)
for i := range largeDesc {
largeDesc[i] = byte('a' + (i % 26))
}
issue := &types.Issue{
Title: "Issue with large description",
Description: string(largeDesc),
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
return store.CreateIssue(ctx, issue, "bench")
})
}
// BenchmarkBulkCloseIssues benchmarks closing 100 issues in sequence
func BenchmarkBulkCloseIssues(b *testing.B) {
store, cleanup := setupLargeBenchDB(b)
defer cleanup()
ctx := context.Background()
// Get 100 open issues to close
openStatus := types.StatusOpen
issues, err := store.SearchIssues(ctx, "", types.IssueFilter{
Status: &openStatus,
Limit: 100,
})
if err != nil || len(issues) < 100 {
b.Fatalf("Failed to get 100 issues for bulk close test: got %d, err %v", len(issues), err)
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for j, issue := range issues {
if err := store.CloseIssue(ctx, issue.ID, "Bulk closed", "bench"); err != nil {
b.Fatalf("CloseIssue failed: %v", err)
}
// Re-open for next iteration (except last one)
if j < len(issues)-1 {
updates := map[string]interface{}{"status": types.StatusOpen}
if err := store.UpdateIssue(ctx, issue.ID, updates, "bench"); err != nil {
b.Fatalf("UpdateIssue failed: %v", err)
}
}
}
}
}
// BenchmarkSyncMerge benchmarks JSONL merge operations (simulating full sync cycle)
func BenchmarkSyncMerge(b *testing.B) {
store, cleanup := setupLargeBenchDB(b)
defer cleanup()
ctx := context.Background()
// For each iteration, simulate a sync by creating and updating issues
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Simulate incoming changes: create 10 new issues, update 10 existing
for j := 0; j < 10; j++ {
issue := &types.Issue{
Title: "Synced issue",
Description: "Incoming change",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "sync"); err != nil {
b.Fatalf("CreateIssue failed: %v", err)
}
}
// Update 10 existing issues
openStatus := types.StatusOpen
issues, err := store.SearchIssues(ctx, "", types.IssueFilter{
Status: &openStatus,
Limit: 10,
})
if err == nil && len(issues) > 0 {
for _, issue := range issues {
updates := map[string]interface{}{
"title": "Updated from sync",
}
_ = store.UpdateIssue(ctx, issue.ID, updates, "sync")
}
}
}
}
// Helper function
func intPtr(i int) *int {
return &i