Files
f2b/main_performance_test.go
Ismo Vuorinen 98b53d84b5 fix: repair Renovate config, convert Makefile to go run, update GitHub Actions (#120)
* fix: repair Renovate config and convert Makefile to go run (#117)

- Remove non-existent `github>renovatebot/presets:golang` preset that
  broke Renovate PR creation
- Replace deprecated `fileMatch` with `managerFilePatterns` in
  customManagers
- Rewrite regex to match new Makefile pattern (renovate comment above
  version variable assignment)
- Fix `matchFileNames` glob pattern (`*.mk` -> `**/*.mk`)
- Convert all tool invocations from `go install` + global binary to
  `go run tool@version` for reproducible builds
- Convert npm global tools to `npx --yes` invocations
- Remove `dev-deps` and `check-deps` targets (tools auto-download)
- Add mdformat pre-commit hook with GFM support and config
- Add `fmt-md` Makefile target for manual markdown formatting
- Update local golangci-lint pre-commit hook to use `go run`
- Apply golangci-lint v2.10.1 auto-fixes (fmt.Fprintf optimization)
- Add nolint:gosec annotations for legitimate exec.Command usage
- Exclude .serena/ from mdformat and megalinter
- Add markdown indent_size=unset in .editorconfig for CommonMark compat

* chore(deps): update GitHub Actions to latest versions

- anthropics/claude-code-action: v1.0.34 -> v1.0.64
- actions/setup-go: v6.2.0 -> v6.3.0
- actions/upload-artifact: v6.0.0 -> v7.0.0
- goreleaser/goreleaser-action: v6.4.0 -> v7.0.0
- docker/login-action: v3.6.0 -> v3.7.0
- ivuorinen/actions: v2026.01.21 -> v2026.02.24

* fix: address code review feedback

- Fix issue template YAML frontmatter (replace underscore separators
  with proper --- delimiters); exclude templates from mdformat
- Replace string(rune(n)) with strconv.Itoa(n) in test files to produce
  deterministic numeric directory names instead of Unicode characters
- Remove stale `make dev-deps` reference in README, replace with
  `make dev-setup`
- Extract ban/unban format strings into shared.MetricsFmtBanOperations
  and shared.MetricsFmtUnbanOperations constants
- Replace hardcoded coverage percentages in README with evergreen
  phrasing

* fix: address round 2 code review feedback for PR #120

- Fix corrupted path traversal example in docs/security.md
- Fix Renovate .mk regex to match nested paths (.*\.mk$)
- Update checkmake pre-commit hook to v0.3.2 to match Makefile
- Add sync.WaitGroup to unsynchronized goroutines in security tests
- Fix fmt-md target to use pre-commit run mdformat
- Pin markdownlint-cli2 to v0.21.0 in lint-md target
- Standardize //nolint:gosec to // #nosec annotations for gosec CLI

* fix(ci): install PyYAML dependency for PR lint workflow

The pr-lint workflow uses ivuorinen/actions/pr-lint which internally
calls validate-inputs running a Python script that imports yaml.
Python was set up but PyYAML was never installed, causing
ModuleNotFoundError at runtime.

* fix: address round 3 code review feedback for PR #120

- Wrap Windows-style path traversal example in backtick code span so
  backslashes render literally in docs/security.md
- Add Renovate-managed MARKDOWNLINT_CLI2_VERSION variable in Makefile
  to match the pattern used by all other tool versions
2026-03-01 19:09:17 +02:00

252 lines
6.9 KiB
Go

package main
import (
"context"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/ivuorinen/f2b/fail2ban"
)
// BenchmarkE2E_MainAPIs benchmarks the main API functions end-to-end
func BenchmarkE2E_MainAPIs(b *testing.B) {
// Setup test environment
tempDir := b.TempDir()
fail2ban.SetLogDir(tempDir)
// Create test log file with realistic content
testLogFile := filepath.Join(tempDir, "fail2ban.log")
testContent := []byte(`2024-01-01 12:00:00,123 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.1.100
2024-01-01 12:01:00,456 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.1.101
2024-01-01 12:02:00,789 fail2ban.actions [1234]: NOTICE [nginx] Ban 192.168.1.102
2024-01-01 12:03:00,012 fail2ban.actions [1234]: NOTICE [sshd] Unban 192.168.1.100
`)
err := os.WriteFile(testLogFile, testContent, 0600)
if err != nil {
b.Fatalf("Failed to create test log file: %v", err)
}
// Setup mock client
client := setupBenchmarkClient(b)
b.Run("GetLogLines", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := fail2ban.GetLogLines(context.Background(), "sshd", "192.168.1.100")
if err != nil {
b.Fatalf("GetLogLines failed: %v", err)
}
}
})
b.Run("GetLogLinesWithLimit", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := fail2ban.GetLogLinesWithLimit(context.Background(), "sshd", "192.168.1.100", 100)
if err != nil {
b.Fatalf("GetLogLinesWithLimit failed: %v", err)
}
}
})
b.Run("UltraOptimized", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := fail2ban.GetLogLinesUltraOptimized("sshd", "192.168.1.100", 100)
if err != nil {
b.Fatalf("GetLogLinesUltraOptimized failed: %v", err)
}
}
})
b.Run("Client_GetBanRecords", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.GetBanRecords([]string{"sshd"})
if err != nil {
b.Fatalf("GetBanRecords failed: %v", err)
}
}
})
b.Run("Client_GetBanRecordsWithContext", func(b *testing.B) {
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := client.GetBanRecordsWithContext(ctx, []string{"sshd"})
if err != nil {
b.Fatalf("GetBanRecordsWithContext failed: %v", err)
}
}
})
}
// BenchmarkMemoryAllocation_Critical benchmarks memory allocations in critical paths
func BenchmarkMemoryAllocation_Critical(b *testing.B) {
tempDir := b.TempDir()
fail2ban.SetLogDir(tempDir)
// Create test log file with proper line structure
testLogFile := filepath.Join(tempDir, "fail2ban.log")
testLine := "2024-01-01 12:00:00,123 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.1.100\n"
// Create 1MB of realistic log data (about 10,000 lines)
var testContent []byte
for len(testContent) < 1024*1024 {
testContent = append(testContent, testLine...)
}
err := os.WriteFile(testLogFile, testContent, 0600)
if err != nil {
b.Fatalf("Failed to create test log file: %v", err)
}
b.Run("LargeLogProcessing", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := fail2ban.GetLogLinesWithLimit(context.Background(), "all", "all", 1000)
if err != nil {
b.Fatalf("Large log processing failed: %v", err)
}
}
})
b.Run("StringPoolingEfficiency", func(b *testing.B) {
processor := fail2ban.NewOptimizedLogProcessor()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := processor.GetLogLinesOptimized("sshd", "192.168.1.100", 100)
if err != nil {
b.Fatalf("String pooling test failed: %v", err)
}
}
})
}
// BenchmarkConcurrentPerformance benchmarks performance under concurrent load
func BenchmarkConcurrentPerformance(b *testing.B) {
tempDir := b.TempDir()
fail2ban.SetLogDir(tempDir)
// Create test log file
testLogFile := filepath.Join(tempDir, "fail2ban.log")
testContent := []byte(`2024-01-01 12:00:00,123 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.1.100
2024-01-01 12:01:00,456 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.1.101
`)
err := os.WriteFile(testLogFile, testContent, 0600)
if err != nil {
b.Fatalf("Failed to create test log file: %v", err)
}
b.Run("ConcurrentLogReading", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := fail2ban.GetLogLinesUltraOptimized("sshd", "all", 50)
if err != nil {
b.Fatalf("Concurrent log reading failed: %v", err)
}
}
})
})
b.Run("ConcurrentCacheAccess", func(b *testing.B) {
processor := fail2ban.NewOptimizedLogProcessor()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Mix cache operations
processor.GetCacheStats()
if pb.Next() {
_, _ = processor.GetLogLinesOptimized("all", "all", 10)
}
}
})
})
}
// BenchmarkGlobalStateAccess benchmarks thread-safe global state access
func BenchmarkGlobalStateAccess(b *testing.B) {
originalLogDir := fail2ban.GetLogDir()
defer fail2ban.SetLogDir(originalLogDir)
b.Run("LogDirAccess", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if pb.Next() {
// 50% reads
fail2ban.GetLogDir()
} else {
// 50% writes
fail2ban.SetLogDir("/tmp/test-" + strconv.Itoa(b.N))
}
}
})
})
}
// setupBenchmarkClient creates a properly configured client for benchmarking
func setupBenchmarkClient(b *testing.B) *fail2ban.RealClient {
b.Helper()
// Setup mock environment
_, cleanup := fail2ban.SetupMockEnvironment(b)
b.Cleanup(cleanup)
// Get the mock runner and add additional responses
mockRunner := fail2ban.GetRunner().(*fail2ban.MockRunner)
mockRunner.SetResponse("fail2ban-client -V", []byte("fail2ban-client v1.0.0"))
mockRunner.SetResponse("fail2ban-client status", []byte("Status: [sshd] Jail list: sshd"))
mockRunner.SetResponse(
"sudo fail2ban-client get sshd banip --with-time",
[]byte(`192.168.1.100 2024-01-01 12:00:00
192.168.1.101 2024-01-01 12:01:00`),
)
client, err := fail2ban.NewClient("", "")
if err != nil {
b.Fatalf("Failed to create test client: %v", err)
}
return client
}
// MemoryProfile runs a memory profiling session
func MemoryProfile(b *testing.B) {
b.Helper()
tempDir := b.TempDir()
fail2ban.SetLogDir(tempDir)
// Create large test log file (10MB)
testLogFile := filepath.Join(tempDir, "fail2ban.log")
testLine := "2024-01-01 12:00:00,123 fail2ban.actions [1234]: NOTICE [sshd] Ban 192.168.1.100\n"
// Create 10MB of realistic log data (about 100,000 lines)
var largeContent []byte
for len(largeContent) < 10*1024*1024 {
largeContent = append(largeContent, testLine...)
}
err := os.WriteFile(testLogFile, largeContent, 0600)
if err != nil {
b.Fatalf("Failed to create large test file: %v", err)
}
b.Run("LargeFileMemoryUsage", func(b *testing.B) {
b.ReportAllocs()
start := time.Now()
for i := 0; i < b.N; i++ {
_, err := fail2ban.GetLogLinesUltraOptimized("all", "all", 10000)
if err != nil {
b.Fatalf("Large file processing failed: %v", err)
}
}
duration := time.Since(start)
b.Logf("Processing 10MB file took %v", duration)
})
}