v0.33.2 57f21d6

history_repo_benchmark_test.go RawHistoryFolder

1package sqlite_test
2
3import (
4	"context"
5	"database/sql"
6	"fmt"
7	"path/filepath"
8	"strings"
9	"testing"
10	"time"
11
12	"github.com/bnema/dumber/internal/application/usecase"
13	"github.com/bnema/dumber/internal/domain/repository"
14	"github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
15	"github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc"
16)
17
18const (
19	// historyBenchmarkCorpusSize is the number of rows seeded for the
20	// recent/search persistence benchmarks.
21	historyBenchmarkCorpusSize = 200
22	// historyBenchmarkEligibleRows is the number of rows left inside the bounded
23	// window; the remainder are stale so bounded benchmarks still exercise a
24	// large history with few eligible rows.
25	historyBenchmarkEligibleRows = 40
26	// historyBenchmarkStaleRows is the number of stale, high-visit-count rows
27	// seeded for bounded most-visited benchmarks. They outrank every eligible
28	// row, so an unfiltered most-visited scan would never reach the eligible rows
29	// within the result limit.
30	historyBenchmarkStaleRows = 2000
31
32	historyBenchmarkRecentLimit      = 50
33	historyBenchmarkSearchLimit      = 10
34	historyBenchmarkMostVisitedLimit = 10
35	historyBenchmarkMaxAgeDays       = 30
36	// historyBenchmarkStaleAgeDays places corpus rows well outside the window.
37	historyBenchmarkStaleAgeDays = 400
38)
39
40// historyBenchmarkRow is one explicitly categorized corpus row. Category is
41// encoded in the URL, title, and timestamp rather than an AUTOINCREMENT id so
42// benchmarks do not depend on insertion order.
43type historyBenchmarkRow struct {
44	url         string
45	title       string
46	lastVisited time.Time
47	visitCount  int
48}
49
50// historyBenchmarkURL returns the stable URL for corpus row i.
51func historyBenchmarkURL(i int) string {
52	return fmt.Sprintf("https://example%d.test/dumber/history/%d", i%20, i)
53}
54
55// newHistoryBenchmarkDB opens an isolated benchmark database and registers
56// cleanup.
57func newHistoryBenchmarkDB(b *testing.B) *sql.DB {
58	b.Helper()
59
60	db, err := sqlite.NewConnection(historyTestCtx(), filepath.Join(b.TempDir(), "dumber.db"))
61	if err != nil {
62		b.Fatal(err)
63	}
64	b.Cleanup(func() {
65		if closeErr := db.Close(); closeErr != nil {
66			b.Errorf("close benchmark database: %v", closeErr)
67		}
68	})
69
70	return db
71}
72
73// seedHistoryBenchmarkRows inserts corpus rows in a single transaction without
74// going through the logging repository path.
75func seedHistoryBenchmarkRows(b *testing.B, ctx context.Context, db *sql.DB, rows []historyBenchmarkRow) {
76	b.Helper()
77
78	tx, err := db.BeginTx(ctx, nil)
79	if err != nil {
80		b.Fatal(err)
81	}
82	stmt, err := tx.PrepareContext(ctx, `INSERT INTO history (url, title, last_visited, visit_count) VALUES (?, ?, ?, ?)`)
83	if err != nil {
84		_ = tx.Rollback()
85		b.Fatal(err)
86	}
87	defer func() { _ = stmt.Close() }()
88
89	for _, row := range rows {
90		if _, err := stmt.ExecContext(
91			ctx,
92			row.url,
93			row.title,
94			sqliteTimestamp(row.lastVisited),
95			row.visitCount,
96		); err != nil {
97			_ = tx.Rollback()
98			b.Fatal(err)
99		}
100	}
101	if err := tx.Commit(); err != nil {
102		b.Fatal(err)
103	}
104}
105
106// historyBenchmarkCorpusRows builds historyBenchmarkCorpusSize rows with the
107// first eligibleRows rows recent and the rest stale.
108func historyBenchmarkCorpusRows(now time.Time, eligibleRows int) []historyBenchmarkRow {
109	recent := now.Add(-time.Hour)
110	stale := now.AddDate(0, 0, -historyBenchmarkStaleAgeDays)
111
112	rows := make([]historyBenchmarkRow, 0, historyBenchmarkCorpusSize)
113	for i := range historyBenchmarkCorpusSize {
114		lastVisited := stale
115		if i < eligibleRows {
116			lastVisited = recent
117		}
118		rows = append(rows, historyBenchmarkRow{
119			url:         historyBenchmarkURL(i),
120			title:       fmt.Sprintf("Dumber history entry %d", i),
121			lastVisited: lastVisited,
122			visitCount:  1,
123		})
124	}
125	return rows
126}
127
128// newHistoryBenchmarkRepository seeds an unbounded corpus where every row is
129// inside the window.
130func newHistoryBenchmarkRepository(b *testing.B) repository.HistoryRepository {
131	b.Helper()
132
133	db := newHistoryBenchmarkDB(b)
134	seedHistoryBenchmarkRows(b, historyTestCtx(), db, historyBenchmarkCorpusRows(time.Now().UTC(), historyBenchmarkCorpusSize))
135
136	return sqlite.NewHistoryRepository(db)
137}
138
139// newBoundedHistoryBenchmarkRepository seeds a corpus where only
140// historyBenchmarkEligibleRows rows fall inside the window, and returns the
141// cutoff for the bounded repository paths.
142func newBoundedHistoryBenchmarkRepository(b *testing.B) (repository.HistoryRepository, time.Time) {
143	b.Helper()
144
145	now := time.Now().UTC()
146	db := newHistoryBenchmarkDB(b)
147	seedHistoryBenchmarkRows(b, historyTestCtx(), db, historyBenchmarkCorpusRows(now, historyBenchmarkEligibleRows))
148
149	return sqlite.NewHistoryRepository(db), now.AddDate(0, 0, -historyBenchmarkMaxAgeDays)
150}
151
152// newMostVisitedBenchmarkUseCase seeds thousands of stale high-count rows plus
153// eligibleRows recent low-count rows, then returns the application use case so
154// benchmarks exercise the real omnibox most-visited path.
155func newMostVisitedBenchmarkUseCase(b *testing.B, eligibleRows int) *usecase.SearchHistoryUseCase {
156	b.Helper()
157
158	now := time.Now().UTC()
159	db := newHistoryBenchmarkDB(b)
160
161	rows := make([]historyBenchmarkRow, 0, historyBenchmarkStaleRows+eligibleRows)
162	for i := range historyBenchmarkStaleRows {
163		rows = append(rows, historyBenchmarkRow{
164			url:         fmt.Sprintf("https://stale%d.test/page", i),
165			title:       fmt.Sprintf("Stale %d", i),
166			lastVisited: now.AddDate(0, 0, -historyBenchmarkStaleAgeDays),
167			visitCount:  10_000 + i,
168		})
169	}
170	for i := range eligibleRows {
171		rows = append(rows, historyBenchmarkRow{
172			url:         fmt.Sprintf("https://eligible%d.test/page", i),
173			title:       fmt.Sprintf("Eligible %d", i),
174			lastVisited: now.Add(-time.Hour),
175			visitCount:  1 + i,
176		})
177	}
178	seedHistoryBenchmarkRows(b, historyTestCtx(), db, rows)
179
180	logBoundedMostVisitedPlan(b, historyTestCtx(), db, now.AddDate(0, 0, -historyBenchmarkMaxAgeDays))
181
182	return usecase.NewSearchHistoryUseCase(sqlite.NewHistoryRepository(db))
183}
184
185// requireBenchmarkLen fails the benchmark if a fixture or query regression
186// changes the expected result count, so benchmarks never silently measure empty
187// work.
188func requireBenchmarkLen(b *testing.B, got, want int, label string) {
189	b.Helper()
190
191	if got != want {
192		b.Fatalf("%s: got %d results, want %d", label, got, want)
193	}
194}
195
196// logBoundedMostVisitedPlan reports (without asserting) the planner choice for
197// the bounded most-visited query, which is informational and not part of the
198// contract.
199func logBoundedMostVisitedPlan(b *testing.B, ctx context.Context, db *sql.DB, cutoff time.Time) {
200	b.Helper()
201
202	rows, err := db.QueryContext(
203		ctx,
204		"EXPLAIN QUERY PLAN "+sqlc.GetMostVisitedHistorySinceCutoff,
205		sqliteTimestamp(cutoff),
206		historyBenchmarkMostVisitedLimit,
207	)
208	if err != nil {
209		b.Logf("bounded most-visited explain failed: %v", err)
210		return
211	}
212	defer func() { _ = rows.Close() }()
213
214	var details []string
215	for rows.Next() {
216		var id, parent, notUsed int
217		var detail string
218		if err := rows.Scan(&id, &parent, &notUsed, &detail); err != nil {
219			b.Logf("bounded most-visited explain scan failed: %v", err)
220			return
221		}
222		details = append(details, detail)
223	}
224	b.Logf("bounded most-visited plan: %s", strings.Join(details, "; "))
225}
226
227func BenchmarkHistorySQLiteSidebarRecent(b *testing.B) {
228	repo := newHistoryBenchmarkRepository(b)
229	ctx := historyTestCtx()
230
231	results, err := repo.GetRecent(ctx, historyBenchmarkRecentLimit, 0, repository.HistoryScope{})
232	if err != nil {
233		b.Fatal(err)
234	}
235	requireBenchmarkLen(b, len(results), historyBenchmarkRecentLimit, "unbounded recent")
236
237	b.ReportAllocs()
238	b.ResetTimer()
239	for range b.N {
240		if _, err := repo.GetRecent(ctx, historyBenchmarkRecentLimit, 0, repository.HistoryScope{}); err != nil {
241			b.Fatal(err)
242		}
243	}
244}
245
246func BenchmarkHistorySQLiteSidebarRecentBounded(b *testing.B) {
247	repo, cutoff := newBoundedHistoryBenchmarkRepository(b)
248	ctx := historyTestCtx()
249	scope := repository.HistoryScope{Cutoff: cutoff}
250
251	results, err := repo.GetRecent(ctx, historyBenchmarkRecentLimit, 0, scope)
252	if err != nil {
253		b.Fatal(err)
254	}
255	requireBenchmarkLen(b, len(results), historyBenchmarkEligibleRows, "bounded recent")
256
257	b.ReportAllocs()
258	b.ResetTimer()
259	for range b.N {
260		if _, err := repo.GetRecent(ctx, historyBenchmarkRecentLimit, 0, scope); err != nil {
261			b.Fatal(err)
262		}
263	}
264}
265
266// BenchmarkHistorySQLiteOmniboxMostVisitedBoundedFewEligible measures the
267// bounded most-visited path when only a handful of rows fall inside the window
268// despite thousands of higher-count stale rows.
269func BenchmarkHistorySQLiteOmniboxMostVisitedBoundedFewEligible(b *testing.B) {
270	const eligibleRows = 5
271
272	uc := newMostVisitedBenchmarkUseCase(b, eligibleRows)
273	ctx := historyTestCtx()
274
275	results, err := uc.GetMostVisitedWithin(ctx, historyBenchmarkMostVisitedLimit, historyBenchmarkMaxAgeDays)
276	if err != nil {
277		b.Fatal(err)
278	}
279	requireBenchmarkLen(b, len(results), eligibleRows, "bounded most-visited few eligible")
280
281	b.ReportAllocs()
282	b.ResetTimer()
283	for range b.N {
284		if _, err := uc.GetMostVisitedWithin(ctx, historyBenchmarkMostVisitedLimit, historyBenchmarkMaxAgeDays); err != nil {
285			b.Fatal(err)
286		}
287	}
288}
289
290// BenchmarkHistorySQLiteOmniboxMostVisitedBoundedNoEligible measures the
291// bounded most-visited path when no row falls inside the window.
292func BenchmarkHistorySQLiteOmniboxMostVisitedBoundedNoEligible(b *testing.B) {
293	uc := newMostVisitedBenchmarkUseCase(b, 0)
294	ctx := historyTestCtx()
295
296	results, err := uc.GetMostVisitedWithin(ctx, historyBenchmarkMostVisitedLimit, historyBenchmarkMaxAgeDays)
297	if err != nil {
298		b.Fatal(err)
299	}
300	requireBenchmarkLen(b, len(results), 0, "bounded most-visited no eligible")
301
302	b.ReportAllocs()
303	b.ResetTimer()
304	for range b.N {
305		if _, err := uc.GetMostVisitedWithin(ctx, historyBenchmarkMostVisitedLimit, historyBenchmarkMaxAgeDays); err != nil {
306			b.Fatal(err)
307		}
308	}
309}
310
311func BenchmarkHistorySQLiteOmniboxSearch(b *testing.B) {
312	repo := newHistoryBenchmarkRepository(b)
313	ctx := historyTestCtx()
314
315	results, err := repo.Search(ctx, "dumber history", historyBenchmarkSearchLimit, repository.HistoryScope{})
316	if err != nil {
317		b.Fatal(err)
318	}
319	requireBenchmarkLen(b, len(results), historyBenchmarkSearchLimit, "unbounded search")
320
321	b.ReportAllocs()
322	b.ResetTimer()
323	for range b.N {
324		if _, err := repo.Search(ctx, "dumber history", historyBenchmarkSearchLimit, repository.HistoryScope{}); err != nil {
325			b.Fatal(err)
326		}
327	}
328}
329
330func BenchmarkHistorySQLiteOmniboxSearchBounded(b *testing.B) {
331	repo, cutoff := newBoundedHistoryBenchmarkRepository(b)
332	ctx := historyTestCtx()
333	scope := repository.HistoryScope{Cutoff: cutoff}
334
335	results, err := repo.Search(ctx, "dumber history", historyBenchmarkSearchLimit, scope)
336	if err != nil {
337		b.Fatal(err)
338	}
339	requireBenchmarkLen(b, len(results), historyBenchmarkSearchLimit, "bounded search")
340
341	b.ReportAllocs()
342	b.ResetTimer()
343	for range b.N {
344		if _, err := repo.Search(ctx, "dumber history", historyBenchmarkSearchLimit, scope); err != nil {
345			b.Fatal(err)
346		}
347	}
348}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close