diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..87a8709 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,29 @@ +name: CodSpeed Performance Benchmarks +on: + push: + branches: + - master + pull_request: null +permissions: + contents: read + id-token: write +jobs: + benchmarks: + name: Run Go Benchmarks with CodSpeed + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.13" + - name: Get dependencies + run: | + go mod download + go mod verify + - name: Run benchmarks with CodSpeed + uses: CodSpeedHQ/action@v4.4.1 + with: + upload-url: ${{ secrets.CODSPEED_STAGING_UPLOAD_URL }} + run: go test -bench=. -benchmem ./... diff --git a/internal/album/service_bench_test.go b/internal/album/service_bench_test.go new file mode 100644 index 0000000..2120f8f --- /dev/null +++ b/internal/album/service_bench_test.go @@ -0,0 +1,119 @@ +package album + +import ( + "context" + "testing" + + "github.com/qiangxue/go-rest-api/pkg/log" +) + +func BenchmarkService_Create(b *testing.B) { + logger, _ := log.NewForTest() + s := NewService(&mockRepository{}, logger) + ctx := context.Background() + req := CreateAlbumRequest{Name: "benchmark album"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = s.Create(ctx, req) + } +} + +func BenchmarkService_Get(b *testing.B) { + logger, _ := log.NewForTest() + repo := &mockRepository{} + s := NewService(repo, logger) + ctx := context.Background() + + // Setup: create an album first + album, _ := s.Create(ctx, CreateAlbumRequest{Name: "test album"}) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = s.Get(ctx, album.ID) + } +} + +func BenchmarkService_Update(b *testing.B) { + logger, _ := log.NewForTest() + repo := &mockRepository{} + s := NewService(repo, logger) + ctx := context.Background() + + // Setup: create an album first + album, _ := s.Create(ctx, CreateAlbumRequest{Name: "test album"}) + req := UpdateAlbumRequest{Name: "updated album"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = s.Update(ctx, album.ID, req) + } +} + +func BenchmarkService_Delete(b *testing.B) { + logger, _ := log.NewForTest() + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + repo := &mockRepository{} + s := NewService(repo, logger) + album, _ := s.Create(ctx, CreateAlbumRequest{Name: "test album"}) + b.StartTimer() + + _, _ = s.Delete(ctx, album.ID) + } +} + +func BenchmarkService_Query(b *testing.B) { + logger, _ := log.NewForTest() + repo := &mockRepository{} + s := NewService(repo, logger) + ctx := context.Background() + + // Setup: create multiple albums + for i := 0; i < 10; i++ { + _, _ = s.Create(ctx, CreateAlbumRequest{Name: "test album"}) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = s.Query(ctx, 0, 10) + } +} + +func BenchmarkService_Count(b *testing.B) { + logger, _ := log.NewForTest() + repo := &mockRepository{} + s := NewService(repo, logger) + ctx := context.Background() + + // Setup: create multiple albums + for i := 0; i < 10; i++ { + _, _ = s.Create(ctx, CreateAlbumRequest{Name: "test album"}) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = s.Count(ctx) + } +} + +func BenchmarkCreateAlbumRequest_Validate(b *testing.B) { + req := CreateAlbumRequest{Name: "test album"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = req.Validate() + } +} + +func BenchmarkUpdateAlbumRequest_Validate(b *testing.B) { + req := UpdateAlbumRequest{Name: "updated album"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = req.Validate() + } +} diff --git a/internal/auth/service_bench_test.go b/internal/auth/service_bench_test.go new file mode 100644 index 0000000..0ed0d77 --- /dev/null +++ b/internal/auth/service_bench_test.go @@ -0,0 +1,48 @@ +package auth + +import ( + "context" + "testing" + + "github.com/qiangxue/go-rest-api/pkg/log" +) + +func BenchmarkService_Login(b *testing.B) { + logger, _ := log.NewForTest() + s := NewService("test-signing-key", 3600, logger) + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = s.Login(ctx, "demo", "pass") + } +} + +func BenchmarkService_LoginInvalid(b *testing.B) { + logger, _ := log.NewForTest() + s := NewService("test-signing-key", 3600, logger) + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = s.Login(ctx, "invalid", "invalid") + } +} + +func BenchmarkWithUser(b *testing.B) { + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = WithUser(ctx, "100", "demo") + } +} + +func BenchmarkCurrentUser(b *testing.B) { + ctx := WithUser(context.Background(), "100", "demo") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = CurrentUser(ctx) + } +} diff --git a/pkg/log/logger_bench_test.go b/pkg/log/logger_bench_test.go new file mode 100644 index 0000000..c91d0a6 --- /dev/null +++ b/pkg/log/logger_bench_test.go @@ -0,0 +1,98 @@ +package log + +import ( + "context" + "net/http" + "testing" +) + +func BenchmarkLogger_With(b *testing.B) { + logger := New() + ctx := context.Background() + args := []interface{}{"key1", "value1", "key2", "value2"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = logger.With(ctx, args...) + } +} + +func BenchmarkLogger_WithContext(b *testing.B) { + logger := New() + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("X-Request-ID", "test-request-id") + req.Header.Set("X-Correlation-ID", "test-correlation-id") + ctx := WithRequest(context.Background(), req) + args := []interface{}{"key", "value"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = logger.With(ctx, args...) + } +} + +func BenchmarkLogger_Debug(b *testing.B) { + logger := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Debug("debug message") + } +} + +func BenchmarkLogger_Info(b *testing.B) { + logger := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Info("info message") + } +} + +func BenchmarkLogger_Error(b *testing.B) { + logger := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Error("error message") + } +} + +func BenchmarkLogger_Debugf(b *testing.B) { + logger := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Debugf("debug message: %s %d", "test", i) + } +} + +func BenchmarkLogger_Infof(b *testing.B) { + logger := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Infof("info message: %s %d", "test", i) + } +} + +func BenchmarkLogger_Errorf(b *testing.B) { + logger := New() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + logger.Errorf("error message: %s %d", "test", i) + } +} + +func BenchmarkWithRequest(b *testing.B) { + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("X-Request-ID", "test-request-id") + req.Header.Set("X-Correlation-ID", "test-correlation-id") + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = WithRequest(ctx, req) + } +} diff --git a/pkg/pagination/pages_bench_test.go b/pkg/pagination/pages_bench_test.go new file mode 100644 index 0000000..05ed4f6 --- /dev/null +++ b/pkg/pagination/pages_bench_test.go @@ -0,0 +1,81 @@ +package pagination + +import ( + "net/http" + "testing" +) + +func BenchmarkNew(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = New(1, 100, 1000) + } +} + +func BenchmarkNewFromRequest(b *testing.B) { + req, _ := http.NewRequest("GET", "/test?page=2&per_page=50", nil) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = NewFromRequest(req, 1000) + } +} + +func BenchmarkPages_Offset(b *testing.B) { + pages := New(5, 100, 1000) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = pages.Offset() + } +} + +func BenchmarkPages_Limit(b *testing.B) { + pages := New(5, 100, 1000) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = pages.Limit() + } +} + +func BenchmarkPages_BuildLinks(b *testing.B) { + pages := New(5, 100, 1000) + baseURL := "http://example.com/api/items" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = pages.BuildLinks(baseURL, 100) + } +} + +func BenchmarkPages_BuildLinkHeader(b *testing.B) { + pages := New(5, 100, 1000) + baseURL := "http://example.com/api/items" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = pages.BuildLinkHeader(baseURL, 100) + } +} + +func BenchmarkParseInt(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = parseInt("42", 10) + } +} + +func BenchmarkParseIntDefault(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = parseInt("", 10) + } +} + +func BenchmarkParseIntInvalid(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = parseInt("invalid", 10) + } +}