mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -05:00
SQLite used one connection for everything, so request lookups queued behind the per-minute stats queries, dashboard pages and eviction scans. Writes keep the single connection. Get, Select, Query and QueryRow now go to a pool of query_only connections (GOMAXPROCS, 2 to 8), which WAL mode lets run alongside the writer. A write sent to that pool fails. Pragmas move into the DSN so every connection gets them. Postgres and in-memory SQLite keep one pool for both.
157 lines
4.8 KiB
Go
157 lines
4.8 KiB
Go
package database
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// benchArtifacts is the artifact count the read pool benchmarks seed: about
|
|
// what a busy cache holds, and enough for the stats queries to take a while.
|
|
const (
|
|
benchPackages = 30_000
|
|
benchVersions = 100_000
|
|
benchArtifacts = 300_000
|
|
)
|
|
|
|
// seedBenchDB creates a database at path with benchArtifacts artifacts
|
|
// spread over three ecosystems, written in one statement per table.
|
|
func seedBenchDB(b *testing.B, path string) {
|
|
b.Helper()
|
|
db, err := Create(path)
|
|
if err != nil {
|
|
b.Fatalf("Create: %v", err)
|
|
}
|
|
defer func() { _ = db.Close() }()
|
|
stmts := []string{
|
|
fmt.Sprintf(`WITH RECURSIVE n(i) AS (SELECT 0 UNION ALL SELECT i+1 FROM n WHERE i+1 < %d)
|
|
INSERT INTO packages (purl, ecosystem, name, created_at, updated_at)
|
|
SELECT 'pkg:' || e || '/p' || i, e, 'p' || i, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
FROM (SELECT i, CASE i %% 3 WHEN 0 THEN 'npm' WHEN 1 THEN 'pypi' ELSE 'cargo' END AS e FROM n)`, benchPackages),
|
|
fmt.Sprintf(`WITH RECURSIVE n(i) AS (SELECT 0 UNION ALL SELECT i+1 FROM n WHERE i+1 < %d)
|
|
INSERT INTO versions (purl, package_purl, integrity, created_at, updated_at)
|
|
SELECT p.purl || '@' || n.i, p.purl, 'sha256-x', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
FROM n JOIN packages p ON p.id = (n.i %% %d) + 1`, benchVersions, benchPackages),
|
|
fmt.Sprintf(`WITH RECURSIVE n(i) AS (SELECT 0 UNION ALL SELECT i+1 FROM n WHERE i+1 < %d)
|
|
INSERT INTO artifacts (version_purl, filename, upstream_url, storage_path, content_hash, size,
|
|
content_type, fetched_at, hit_count, last_accessed_at, created_at, updated_at)
|
|
SELECT v.purl, 'f' || n.i, 'https://example.org/' || n.i, 'blobs/' || n.i, 'h', 1000 + n.i,
|
|
'application/octet-stream', CURRENT_TIMESTAMP, n.i %% 7, CURRENT_TIMESTAMP,
|
|
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
|
FROM n JOIN versions v ON v.id = (n.i %% %d) + 1`, benchArtifacts, benchVersions),
|
|
}
|
|
for _, s := range stmts {
|
|
if _, err := db.Exec(s); err != nil {
|
|
b.Fatalf("seeding: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// openBenchDB opens path. With oneConn it routes reads to the writer, as
|
|
// before the read pool existed.
|
|
func openBenchDB(b *testing.B, path string, oneConn bool) *DB {
|
|
b.Helper()
|
|
db, err := Open(path)
|
|
if err != nil {
|
|
b.Fatalf("Open: %v", err)
|
|
}
|
|
if oneConn {
|
|
_ = db.reader.Close()
|
|
db.reader = db.DB
|
|
}
|
|
b.Cleanup(func() { _ = db.Close() })
|
|
return db
|
|
}
|
|
|
|
// benchKey returns the lookup arguments for artifact i, as seeded above.
|
|
func benchKey(i int) (pkgPURL, versionPURL, filename string) {
|
|
v := i % benchVersions
|
|
p := v % benchPackages
|
|
eco := [...]string{"npm", "pypi", "cargo"}[p%3]
|
|
pkgPURL = fmt.Sprintf("pkg:%s/p%d", eco, p)
|
|
return pkgPURL, fmt.Sprintf("%s@%d", pkgPURL, v), fmt.Sprintf("f%d", i)
|
|
}
|
|
|
|
func lookup(b *testing.B, db *DB, i int) {
|
|
pkgPURL, versionPURL, filename := benchKey(i % benchArtifacts)
|
|
art, err := db.GetCachedArtifact(pkgPURL, versionPURL, filename)
|
|
if err != nil || art == nil {
|
|
b.Fatalf("GetCachedArtifact(%s, %s) = %v, %v", versionPURL, filename, art, err)
|
|
}
|
|
}
|
|
|
|
var poolModes = []struct {
|
|
name string
|
|
oneConn bool
|
|
}{
|
|
{"one-connection", true},
|
|
{"read-pool", false},
|
|
}
|
|
|
|
// BenchmarkSQLiteParallelLookups runs GetCachedArtifact from parallel
|
|
// goroutines, as concurrent downloads do.
|
|
func BenchmarkSQLiteParallelLookups(b *testing.B) {
|
|
path := filepath.Join(b.TempDir(), "bench.db")
|
|
seedBenchDB(b, path)
|
|
for _, mode := range poolModes {
|
|
b.Run(mode.name, func(b *testing.B) {
|
|
db := openBenchDB(b, path, mode.oneConn)
|
|
var next atomic.Int64
|
|
b.ResetTimer()
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
for pb.Next() {
|
|
lookup(b, db, int(next.Add(7919)))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkSQLiteLookupDuringStats runs GetCachedArtifact one at a time
|
|
// while another goroutine keeps running GetEcosystemStats and GetCacheStats,
|
|
// as the metrics updater and the dashboard do. It reports the slowest lookup.
|
|
func BenchmarkSQLiteLookupDuringStats(b *testing.B) {
|
|
path := filepath.Join(b.TempDir(), "bench.db")
|
|
seedBenchDB(b, path)
|
|
for _, mode := range poolModes {
|
|
b.Run(mode.name, func(b *testing.B) {
|
|
db := openBenchDB(b, path, mode.oneConn)
|
|
stop := make(chan struct{})
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
}
|
|
if _, err := db.GetEcosystemStats(); err != nil {
|
|
b.Errorf("GetEcosystemStats: %v", err)
|
|
return
|
|
}
|
|
if _, err := db.GetCacheStats(); err != nil {
|
|
b.Errorf("GetCacheStats: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
var slowest time.Duration
|
|
b.ResetTimer()
|
|
for i := range b.N {
|
|
start := time.Now()
|
|
lookup(b, db, i*7919)
|
|
slowest = max(slowest, time.Since(start))
|
|
}
|
|
b.StopTimer()
|
|
close(stop)
|
|
wg.Wait()
|
|
b.ReportMetric(float64(slowest.Microseconds())/1000, "max-ms")
|
|
})
|
|
}
|
|
}
|