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:
1604
.beads/issues.jsonl
1604
.beads/issues.jsonl
File diff suppressed because one or more lines are too long
140
BENCHMARKS.md
Normal file
140
BENCHMARKS.md
Normal 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/...
|
||||
```
|
||||
@@ -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
269
cmd/bd/thanks.go
Normal 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
11
go.mod
@@ -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
23
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user