From d4caf45b2b9ea6a3276de792cf6f73085e66b1ae Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 18:15:47 +0000 Subject: Add performance benchmarks, stress tests, and frontend perf tests Go benchmarks cover item CRUD/filter/sanitization, API endpoints (stream, item update, feed list), middleware stack (gzip, security headers, CSRF), and crawler pipeline (feed parsing, mocked crawl). Stress tests verify concurrent reads/writes and large dataset handling. Frontend perf tests measure template generation, DOM insertion, and store event throughput. New Makefile targets: bench, bench-short, stress, test-perf. https://claude.ai/code/session_01ChDVWFDrQoFjMYHpaLGr9s --- models/item/item_bench_test.go | 219 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 models/item/item_bench_test.go (limited to 'models/item/item_bench_test.go') diff --git a/models/item/item_bench_test.go b/models/item/item_bench_test.go new file mode 100644 index 0000000..5e66f2d --- /dev/null +++ b/models/item/item_bench_test.go @@ -0,0 +1,219 @@ +package item + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "adammathes.com/neko/config" + "adammathes.com/neko/models" +) + +func setupBenchDB(b *testing.B) { + b.Helper() + config.Config.DBFile = filepath.Join(b.TempDir(), "bench.db") + models.InitDB() + b.Cleanup(func() { + if models.DB != nil { + models.DB.Close() + } + }) +} + +func createBenchFeed(b *testing.B) int64 { + b.Helper() + res, err := models.DB.Exec("INSERT INTO feed(url, title, category) VALUES(?, ?, ?)", + "https://example.com/feed", "Bench Feed", "tech") + if err != nil { + b.Fatal(err) + } + id, _ := res.LastInsertId() + return id +} + +func seedBenchItems(b *testing.B, feedID int64, count int) { + b.Helper() + for i := 0; i < count; i++ { + _, err := models.DB.Exec( + `INSERT INTO item(title, url, description, publish_date, feed_id, read_state, starred) + VALUES(?, ?, ?, datetime('now'), ?, 0, 0)`, + fmt.Sprintf("Bench Item %d", i), + fmt.Sprintf("https://example.com/item/%d", i), + fmt.Sprintf("

Description for item %d with bold and link

", i), + feedID, + ) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkItemCreate(b *testing.B) { + setupBenchDB(b) + feedID := createBenchFeed(b) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + item := &Item{ + Title: fmt.Sprintf("Item %d", i), + Url: fmt.Sprintf("https://example.com/bench/%d", i), + Description: "

Benchmark item description

", + PublishDate: "2024-01-01 00:00:00", + FeedId: feedID, + } + _ = item.Create() + } +} + +func BenchmarkItemCreateBatch100(b *testing.B) { + setupBenchDB(b) + feedID := createBenchFeed(b) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 100; j++ { + item := &Item{ + Title: fmt.Sprintf("Batch %d Item %d", i, j), + Url: fmt.Sprintf("https://example.com/batch/%d/%d", i, j), + Description: "

Batch item description

", + PublishDate: "2024-01-01 00:00:00", + FeedId: feedID, + } + _ = item.Create() + } + } +} + +func BenchmarkFilter_Empty(b *testing.B) { + setupBenchDB(b) + _ = createBenchFeed(b) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = Filter(0, nil, "", false, false, 0, "") + } +} + +func BenchmarkFilter_15Items(b *testing.B) { + setupBenchDB(b) + feedID := createBenchFeed(b) + seedBenchItems(b, feedID, 15) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = Filter(0, nil, "", false, false, 0, "") + } +} + +func BenchmarkFilter_WithFTS(b *testing.B) { + setupBenchDB(b) + feedID := createBenchFeed(b) + seedBenchItems(b, feedID, 50) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = Filter(0, nil, "", false, false, 0, "Bench") + } +} + +func BenchmarkFilter_WithImageProxy(b *testing.B) { + setupBenchDB(b) + feedID := createBenchFeed(b) + + // Seed items with image-heavy descriptions + for i := 0; i < 15; i++ { + _, err := models.DB.Exec( + `INSERT INTO item(title, url, description, publish_date, feed_id, read_state, starred) + VALUES(?, ?, ?, datetime('now'), ?, 0, 0)`, + fmt.Sprintf("Image Item %d", i), + fmt.Sprintf("https://example.com/img/%d", i), + `

Text with images a and b

`, + feedID, + ) + if err != nil { + b.Fatal(err) + } + } + + config.Config.ProxyImages = true + b.Cleanup(func() { config.Config.ProxyImages = false }) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = Filter(0, nil, "", false, false, 0, "") + } +} + +func BenchmarkFilterPolicy(b *testing.B) { + html := `

Hello world with link and test and

` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + p := filterPolicy() + _ = p.Sanitize(html) + } +} + +func BenchmarkRewriteImages(b *testing.B) { + html := `

Text 1 more text + 2 + 3 + 4 + 5

` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = rewriteImages(html) + } +} + +func BenchmarkItemSave(b *testing.B) { + setupBenchDB(b) + feedID := createBenchFeed(b) + + item := &Item{ + Title: "Save Bench Item", + Url: "https://example.com/save-bench", + Description: "

Item to update

", + PublishDate: "2024-01-01 00:00:00", + FeedId: feedID, + } + if err := item.Create(); err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + item.ReadState = !item.ReadState + item.Save() + } +} + +func BenchmarkFilter_LargeDataset(b *testing.B) { + setupBenchDB(b) + feedID := createBenchFeed(b) + + // Bulk insert 500 items for a realistic dataset + var sb strings.Builder + for i := 0; i < 500; i++ { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(fmt.Sprintf( + "('Item %d', 'https://example.com/large/%d', '

Description %d

', datetime('now'), %d, 0, 0)", + i, i, i, feedID, + )) + } + _, err := models.DB.Exec( + "INSERT INTO item(title, url, description, publish_date, feed_id, read_state, starred) VALUES " + sb.String(), + ) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = Filter(0, nil, "", false, false, 0, "") + } +} -- cgit v1.2.3