aboutsummaryrefslogtreecommitdiffstats
path: root/models/item
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-02-16 18:15:47 +0000
committerClaude <noreply@anthropic.com>2026-02-16 18:15:47 +0000
commitd4caf45b2b9ea6a3276de792cf6f73085e66b1ae (patch)
tree3a8b7b1d9277ba0db4c946fea43043d65220fd8d /models/item
parentc9c5def76c3a3340373143f846454b795d296c82 (diff)
downloadneko-d4caf45b2b9ea6a3276de792cf6f73085e66b1ae.tar.gz
neko-d4caf45b2b9ea6a3276de792cf6f73085e66b1ae.tar.bz2
neko-d4caf45b2b9ea6a3276de792cf6f73085e66b1ae.zip
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
Diffstat (limited to 'models/item')
-rw-r--r--models/item/item_bench_test.go219
1 files changed, 219 insertions, 0 deletions
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("<p>Description for item %d with <b>bold</b> and <a href='https://example.com'>link</a></p>", 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: "<p>Benchmark item description</p>",
+ 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: "<p>Batch item description</p>",
+ 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),
+ `<p>Text with images <img src="https://example.com/a.jpg" alt="a"> and <img src="https://example.com/b.png" alt="b"></p>`,
+ 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 := `<p>Hello <b>world</b> with <a href="https://example.com">link</a> and <img src="https://example.com/img.jpg" alt="test"> and <script>alert('xss')</script></p>`
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ p := filterPolicy()
+ _ = p.Sanitize(html)
+ }
+}
+
+func BenchmarkRewriteImages(b *testing.B) {
+ html := `<p>Text <img src="https://example.com/1.jpg" alt="1"> more text
+ <img src="https://example.com/2.png" alt="2">
+ <img src="https://example.com/3.gif" alt="3">
+ <img src="https://example.com/4.webp" alt="4" srcset="https://example.com/4-2x.webp 2x, https://example.com/4-3x.webp 3x">
+ <img src="https://example.com/5.jpg" alt="5"></p>`
+
+ 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: "<p>Item to update</p>",
+ 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', '<p>Description %d</p>', 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, "")
+ }
+}