Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,60 @@ tasks:
desc: "Show assistant availability for a schedule (usage: task visualize:availability -- <id>)"
cmds:
- ./scripts/visualize.sh availability {{.CLI_ARGS}}

# Benchmarks
bench:backend:
desc: Run backend Go benchmarks (repository, service, handler, e2e)
dir: "{{.BACKEND_DIR}}"
cmds:
- go test -bench=. -benchmem -count=3 -run=^$ -timeout=30m ./internal/tests/benchmarks/...

bench:backend:repo:
desc: Run backend repository benchmarks only
dir: "{{.BACKEND_DIR}}"
cmds:
- go test -bench=. -benchmem -count=3 -run=^$ -timeout=30m ./internal/tests/benchmarks/repository/...

bench:backend:service:
desc: Run backend service benchmarks only
dir: "{{.BACKEND_DIR}}"
cmds:
- go test -bench=. -benchmem -count=3 -run=^$ -timeout=10m ./internal/tests/benchmarks/service/...

bench:backend:handler:
desc: Run backend handler benchmarks only
dir: "{{.BACKEND_DIR}}"
cmds:
- go test -bench=. -benchmem -count=3 -run=^$ -timeout=10m ./internal/tests/benchmarks/handler/...

bench:backend:e2e:
desc: Run backend E2E benchmarks only
dir: "{{.BACKEND_DIR}}"
cmds:
- go test -bench=. -benchmem -count=3 -run=^$ -timeout=30m ./internal/tests/benchmarks/e2e/...

bench:scheduler:
desc: Run scheduler HTTP benchmarks (requires scheduler service running on :8001)
dir: ./tests/benchmarks
cmds:
- go test -bench=. -benchmem -count=3 -run=^$ -timeout=30m ./scheduler/...

bench:transcripts:
desc: Run transcripts HTTP benchmarks (requires transcripts service running on :8002)
dir: ./tests/benchmarks
cmds:
- go test -bench=. -benchmem -count=3 -run=^$ -timeout=30m ./transcripts/...

bench:services:
desc: Start Python services and run HTTP benchmarks
cmds:
- docker compose -f docker-compose.bench.yml up -d --wait
- task: bench:scheduler
- task: bench:transcripts
- docker compose -f docker-compose.bench.yml down
Comment thread
jaden5677 marked this conversation as resolved.

bench:all:
desc: Run all benchmarks (backend + scheduler + transcripts)
cmds:
- task: bench:backend
- task: bench:services
145 changes: 145 additions & 0 deletions apps/backend/internal/tests/benchmarks/bench_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package benchmarks

import (
"context"
"database/sql"
"fmt"
"path/filepath"
"runtime"
"sync"
"testing"
"time"

_ "github.com/lib/pq"
"github.com/pressly/goose/v3"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
"go.uber.org/zap"

"github.com/HDR3604/HelpDeskApp/internal/infrastructure/database"
)

// BenchDB wraps a testcontainers PostgreSQL instance for benchmark tests.
type BenchDB struct {
DB *sql.DB
Logger *zap.Logger
TxManager database.TxManagerInterface
ctx context.Context
container *postgres.PostgresContainer
}

var (
sharedDB *BenchDB
sharedOnce sync.Once
sharedErr error
)

// SharedBenchDB returns a single shared PostgreSQL container reused across all
// benchmarks in the same test binary. The container is created once (with
// migrations) on the first call and never torn down until the process exits,
// which is handled automatically by testcontainers' reaper.
func SharedBenchDB(b *testing.B) *BenchDB {
b.Helper()
sharedOnce.Do(func() {
sharedDB, sharedErr = newBenchDB()
})
if sharedErr != nil {
b.Fatalf("shared BenchDB: %v", sharedErr)
}
return sharedDB
}

// newBenchDB starts a PostgreSQL container and runs all migrations.
func newBenchDB() (*BenchDB, error) {
ctx := context.Background()

_, currentFile, _, _ := runtime.Caller(0)
migrationsDir := filepath.Join(filepath.Dir(currentFile), "..", "..", "..", "..", "..", "migrations")

pgContainer, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("helpdesk"),
postgres.WithUsername("helpdesk"),
postgres.WithPassword("helpdesk"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(60*time.Second),
),
)
if err != nil {
return nil, fmt.Errorf("failed to start postgres container: %w", err)
}

connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
if err != nil {
return nil, fmt.Errorf("failed to get connection string: %w", err)
}

db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}

if err := goose.SetDialect("postgres"); err != nil {
return nil, fmt.Errorf("failed to set goose dialect: %w", err)
}
if err := goose.Up(db, migrationsDir); err != nil {
return nil, fmt.Errorf("failed to run migrations: %w", err)
}

logger := zap.NewNop()
txManager := database.NewTxManager(db, logger)

return &BenchDB{
DB: db,
Logger: logger,
TxManager: txManager,
ctx: ctx,
container: pgContainer,
}, nil
}

// NewBenchDB starts a dedicated PostgreSQL container for a single benchmark.
// Prefer SharedBenchDB when multiple benchmarks can share one container.
func NewBenchDB(b *testing.B) *BenchDB {
b.Helper()
bdb, err := newBenchDB()
if err != nil {
b.Fatal(err)
}
b.Cleanup(func() { bdb.close() })
return bdb
}

func (bdb *BenchDB) close() {
if bdb.DB != nil {
_ = bdb.DB.Close()
}
if bdb.container != nil {
_ = bdb.container.Terminate(bdb.ctx)
}
}

// DeleteAll removes rows from the given tables within an InSystemTx.
// Use this between benchmark iterations to reset state.
func (bdb *BenchDB) DeleteAll(b *testing.B, tables ...string) {
b.Helper()
err := bdb.TxManager.InSystemTx(bdb.ctx, func(tx *sql.Tx) error {
for _, t := range tables {
if _, err := tx.ExecContext(bdb.ctx, fmt.Sprintf("DELETE FROM %s", t)); err != nil {
return fmt.Errorf("delete from %s: %w", t, err)
}
}
return nil
})
if err != nil {
b.Fatalf("DeleteAll failed: %v", err)
}
}

// Ctx returns the background context used by the BenchDB.
func (bdb *BenchDB) Ctx() context.Context {
return bdb.ctx
}
Loading
Loading