Documentation

Performance

Overview

Performance optimization is crucial for delivering fast, responsive applications. This guide covers techniques for optimizing database queries, implementing efficient caching strategies, and monitoring application performance.

Database Optimization

Query Optimization

// ✅ Efficient - Use specific columns
func (f *GardenFetcher) FindGardenSummaries(ctx context.Context) ([]*GardenSummary, error) {
    query := `
        SELECT id, name, created_at, user_id,
               (SELECT COUNT(*) FROM plants WHERE garden_id = gardens.id) as plant_count
        FROM gardens
        WHERE active = TRUE
        ORDER BY created_at DESC
    `

    rows, err := f.db.QueryContext(ctx, query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var summaries []*GardenSummary
    for rows.Next() {
        summary := &GardenSummary{}
        err := rows.Scan(&summary.Id, &summary.Name, &summary.CreatedAt,
                        &summary.UserId, &summary.PlantCount)
        if err != nil {
            return nil, err
        }
        summaries = append(summaries, summary)
    }

    return summaries, nil
}

// ❌ Inefficient - Loading full entities and counting in Go
func (f *GardenFetcher) FindGardenSummariesSlow(ctx context.Context) ([]*GardenSummary, error) {
    gardens, err := f.FindAll(ctx, nil)
    if err != nil {
        return nil, err
    }

    var summaries []*GardenSummary
    for _, garden := range gardens {
        plants, err := f.plantFetcher.FindByGardenId(ctx, garden.Id)
        if err != nil {
            return nil, err
        }

        summary := &GardenSummary{
            Id:         garden.Id,
            Name:       garden.Name,
            CreatedAt:  garden.CreatedAt,
            PlantCount: len(plants),
        }
        summaries = append(summaries, summary)
    }

    return summaries, nil
}

Batch Operations

// Efficient batch insert
func (h *PlantHandler) CreateMany(ctx context.Context, plants []*mdl.Plant, mod *handler.HandlerMod) error {
    if len(plants) == 0 {
        return nil
    }

    // Build batch insert query
    query := "INSERT INTO plants (id, name, species, garden_id, created_at, updated_at) VALUES "
    args := make([]interface{}, 0, len(plants)*6)
    placeholders := make([]string, 0, len(plants))

    for _, plant := range plants {
        plant.Id = generateID()
        plant.CreatedAt = time.Now()
        plant.UpdatedAt = time.Now()

        placeholders = append(placeholders, "(?, ?, ?, ?, ?, ?)")
        args = append(args, plant.Id, plant.Name, plant.Species,
                     plant.GardenId, plant.CreatedAt, plant.UpdatedAt)
    }

    query += strings.Join(placeholders, ", ")

    _, err := h.db.ExecContext(ctx, query, args...)
    return err
}

// Efficient batch update
func (h *PlantHandler) UpdateManyHeights(ctx context.Context, updates map[string]float64) error {
    if len(updates) == 0 {
        return nil
    }

    query := "UPDATE plants SET height = ?, updated_at = ? WHERE id = ?"
    stmt, err := h.db.PrepareContext(ctx, query)
    if err != nil {
        return err
    }
    defer stmt.Close()

    now := time.Now()
    for id, height := range updates {
        _, err = stmt.ExecContext(ctx, height, now, id)
        if err != nil {
            return err
        }
    }

    return nil
}

Connection Pool Optimization

func setupDatabase(config *config.Database) *sql.DB {
    db, err := sql.Open(config.Driver, config.URL)
    if err != nil {
        log.Fatal(err)
    }

    // Connection pool settings
    db.SetMaxOpenConns(25)              // Maximum open connections
    db.SetMaxIdleConns(5)               // Maximum idle connections
    db.SetConnMaxLifetime(5 * time.Minute) // Connection lifetime
    db.SetConnMaxIdleTime(1 * time.Minute) // Idle timeout

    // Verify connection
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    return db
}

Memory Optimization

Efficient Data Structures

// Use struct pointers for large objects
type GardenWithPlants struct {
    Garden *mdl.Garden  // Pointer to avoid copying
    Plants []*mdl.Plant // Slice of pointers
}

// Pool objects for reuse
var gardenPool = sync.Pool{
    New: func() interface{} {
        return &mdl.Garden{}
    },
}

func (f *GardenFetcher) FindOneByIdPooled(ctx context.Context, id string) (*mdl.Garden, bool, error) {
    garden := gardenPool.Get().(*mdl.Garden)
    defer gardenPool.Put(garden)

    // Reset all fields
    *garden = mdl.Garden{}

    err := f.db.QueryRowContext(ctx, "SELECT * FROM gardens WHERE id = ?", id).
        Scan(&garden.Id, &garden.Name, &garden.Description, &garden.CreatedAt, &garden.UpdatedAt)

    if err == sql.ErrNoRows {
        return nil, false, nil
    }
    if err != nil {
        return nil, false, err
    }

    // Create new instance to return
    result := &mdl.Garden{}
    *result = *garden

    return result, true, nil
}

Memory Profiling

import (
    _ "net/http/pprof"
    "net/http"
)

// Enable profiling endpoint
func setupProfiling(router *gin.Engine) {
    // Add pprof routes in development
    if gin.Mode() == gin.DebugMode {
        pprof := router.Group("/debug/pprof")
        {
            pprof.GET("/", gin.WrapF(http.HandlerFunc(pprof.Index)))
            pprof.GET("/heap", gin.WrapF(http.HandlerFunc(pprof.Handler("heap").ServeHTTP)))
            pprof.GET("/goroutine", gin.WrapF(http.HandlerFunc(pprof.Handler("goroutine").ServeHTTP)))
            pprof.GET("/allocs", gin.WrapF(http.HandlerFunc(pprof.Handler("allocs").ServeHTTP)))
        }
    }
}

// Memory monitoring
func monitorMemory() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)

    log.Printf("Memory - Alloc: %d KB, TotalAlloc: %d KB, Sys: %d KB, NumGC: %d",
        bToKb(m.Alloc), bToKb(m.TotalAlloc), bToKb(m.Sys), m.NumGC)
}

func bToKb(b uint64) uint64 {
    return b / 1024
}

Concurrency Optimization

Worker Pools

type WorkerPool struct {
    workerCount int
    jobQueue    chan Job
    quit        chan bool
}

type Job func() error

func NewWorkerPool(workerCount int, queueSize int) *WorkerPool {
    return &WorkerPool{
        workerCount: workerCount,
        jobQueue:    make(chan Job, queueSize),
        quit:        make(chan bool),
    }
}

func (wp *WorkerPool) Start() {
    for i := 0; i < wp.workerCount; i++ {
        go wp.worker()
    }
}

func (wp *WorkerPool) worker() {
    for {
        select {
        case job := <-wp.jobQueue:
            if err := job(); err != nil {
                log.Printf("Worker job failed: %v", err)
            }
        case <-wp.quit:
            return
        }
    }
}

func (wp *WorkerPool) Submit(job Job) {
    select {
    case wp.jobQueue <- job:
    default:
        log.Println("Job queue is full, dropping job")
    }
}

// Usage for background processing
func (h *EmailHandler) SendWelcomeEmail(userID string) {
    job := func() error {
        user, exists, err := h.userFetcher.FindOneById(context.Background(), userID, nil)
        if err != nil || !exists {
            return err
        }

        return h.emailService.SendWelcomeEmail(user.Email, user.Name)
    }

    h.workerPool.Submit(job)
}

Goroutine Management

// Bounded goroutine execution
func (f *GardenFetcher) LoadGardensWithPlantsParallel(ctx context.Context, gardenIDs []string) ([]*GardenWithPlants, error) {
    const maxConcurrency = 10
    semaphore := make(chan struct{}, maxConcurrency)

    results := make([]*GardenWithPlants, len(gardenIDs))
    errors := make([]error, len(gardenIDs))
    var wg sync.WaitGroup

    for i, gardenID := range gardenIDs {
        wg.Add(1)
        go func(index int, id string) {
            defer wg.Done()

            // Acquire semaphore
            semaphore <- struct{}{}
            defer func() { <-semaphore }()

            garden, exists, err := f.FindOneById(ctx, id, nil)
            if err != nil || !exists {
                errors[index] = err
                return
            }

            plants, err := f.plantFetcher.FindByGardenId(ctx, id)
            if err != nil {
                errors[index] = err
                return
            }

            results[index] = &GardenWithPlants{
                Garden: garden,
                Plants: plants,
            }
        }(i, gardenID)
    }

    wg.Wait()

    // Check for errors
    for _, err := range errors {
        if err != nil {
            return nil, err
        }
    }

    return results, nil
}

HTTP Performance

Response Compression

import "github.com/gin-contrib/gzip"

func setupCompression(router *gin.Engine) {
    // Enable gzip compression
    router.Use(gzip.Gzip(gzip.DefaultCompression))
}

// Custom compression for large responses
func CompressJSONResponse(data interface{}) ([]byte, error) {
    jsonData, err := json.Marshal(data)
    if err != nil {
        return nil, err
    }

    var buf bytes.Buffer
    gzipWriter := gzip.NewWriter(&buf)

    _, err = gzipWriter.Write(jsonData)
    if err != nil {
        return nil, err
    }

    err = gzipWriter.Close()
    if err != nil {
        return nil, err
    }

    return buf.Bytes(), nil
}

HTTP/2 Server Push

func setupHTTP2Push(router *gin.Engine) {
    router.GET("/gardens/:id", func(c *gin.Context) {
        // Push critical resources
        if pusher := c.Writer.Pusher(); pusher != nil {
            pusher.Push("/assets/css/output.css", nil)
            pusher.Push("/assets/js/app.js", nil)
        }

        // Render response
        // ...
    })
}

Monitoring & Metrics

Performance Metrics

type MetricsCollector struct {
    requestCount    *prometheus.CounterVec
    requestDuration *prometheus.HistogramVec
    activeRequests  prometheus.Gauge
}

func NewMetricsCollector() *MetricsCollector {
    return &MetricsCollector{
        requestCount: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "http_requests_total",
                Help: "Total HTTP requests",
            },
            []string{"method", "endpoint", "status"},
        ),
        requestDuration: prometheus.NewHistogramVec(
            prometheus.HistogramOpts{
                Name: "http_request_duration_seconds",
                Help: "HTTP request duration in seconds",
            },
            []string{"method", "endpoint"},
        ),
        activeRequests: prometheus.NewGauge(
            prometheus.GaugeOpts{
                Name: "http_requests_active",
                Help: "Active HTTP requests",
            },
        ),
    }
}

func (mc *MetricsCollector) MetricsMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        mc.activeRequests.Inc()

        defer func() {
            duration := time.Since(start).Seconds()
            mc.activeRequests.Dec()

            mc.requestCount.WithLabelValues(
                c.Request.Method,
                c.FullPath(),
                fmt.Sprintf("%d", c.Writer.Status()),
            ).Inc()

            mc.requestDuration.WithLabelValues(
                c.Request.Method,
                c.FullPath(),
            ).Observe(duration)
        }()

        c.Next()
    }
}

Database Query Monitoring

type QueryLogger struct {
    slowQueryThreshold time.Duration
    logger            *log.Logger
}

func NewQueryLogger(threshold time.Duration, logger *log.Logger) *QueryLogger {
    return &QueryLogger{
        slowQueryThreshold: threshold,
        logger:            logger,
    }
}

func (ql *QueryLogger) LogQuery(query string, args []interface{}, duration time.Duration) {
    if duration > ql.slowQueryThreshold {
        ql.logger.Printf("SLOW QUERY (%v): %s [args: %v]", duration, query, args)
    }
}

// Wrapper for database queries
func (f *GardenFetcher) QueryWithLogging(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
    start := time.Now()
    rows, err := f.db.QueryContext(ctx, query, args...)
    duration := time.Since(start)

    f.queryLogger.LogQuery(query, args, duration)

    return rows, err
}

Load Testing

Benchmark Tests

func BenchmarkGardenFetch(b *testing.B) {
    fetcher := setupBenchmarkFetcher()
    ctx := context.Background()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _, err := fetcher.FindOneById(ctx, "test-garden-id", nil)
        if err != nil {
            b.Fatal(err)
        }
    }
}

func BenchmarkGardenFetchParallel(b *testing.B) {
    fetcher := setupBenchmarkFetcher()
    ctx := context.Background()

    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _, _, err := fetcher.FindOneById(ctx, "test-garden-id", nil)
            if err != nil {
                b.Fatal(err)
            }
        }
    })
}

Load Testing Script

// Simple load test
func loadTest() {
    const (
        baseURL     = "http://localhost:8080"
        concurrency = 10
        requests    = 1000
    )

    client := &http.Client{
        Timeout: 30 * time.Second,
    }

    var wg sync.WaitGroup
    requestsPerWorker := requests / concurrency
    results := make(chan time.Duration, requests)

    for i := 0; i < concurrency; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()

            for j := 0; j < requestsPerWorker; j++ {
                start := time.Now()
                resp, err := client.Get(baseURL + "/api/gardens")
                duration := time.Since(start)

                if err != nil {
                    log.Printf("Request failed: %v", err)
                    continue
                }
                resp.Body.Close()

                results <- duration
            }
        }()
    }

    wg.Wait()
    close(results)

    // Calculate statistics
    var durations []time.Duration
    for duration := range results {
        durations = append(durations, duration)
    }

    sort.Slice(durations, func(i, j int) bool {
        return durations[i] < durations[j]
    })

    fmt.Printf("Total requests: %d\n", len(durations))
    fmt.Printf("Average: %v\n", calculateAverage(durations))
    fmt.Printf("95th percentile: %v\n", durations[int(float64(len(durations))*0.95)])
    fmt.Printf("99th percentile: %v\n", durations[int(float64(len(durations))*0.99)])
}

Performance Best Practices

General Guidelines

  1. Profile First: Always measure before optimizing
  2. Database Efficiency: Optimize queries and use appropriate indexes
  3. Caching Strategy: Implement multi-level caching
  4. Connection Pooling: Configure database connection pools properly
  5. Concurrent Processing: Use goroutines and worker pools for I/O bound tasks
  6. Memory Management: Use object pools and avoid memory leaks
  7. HTTP Optimization: Enable compression and use HTTP/2
  8. Monitoring: Implement comprehensive monitoring and alerting

Common Anti-patterns

// ❌ N+1 Query Problem
for _, garden := range gardens {
    plants, _ := plantFetcher.FindByGardenId(ctx, garden.Id)
    // Process plants...
}

// ✅ Batch Loading
allPlants, _ := plantFetcher.FindByGardenIds(ctx, gardenIds)
plantsByGarden := groupPlantsByGarden(allPlants)

// ❌ Loading Too Much Data
gardens, _ := gardenFetcher.FindAll(ctx, nil) // Loads everything

// ✅ Pagination and Filtering
mod := gardenFetcher.Mod()
mod.Page = 1
mod.PerPage = 20
gardens, pagination, _ := gardenFetcher.FindPage(ctx, mod)

// ❌ Synchronous Blocking Operations
for _, email := range emails {
    emailService.Send(email) // Blocks for each email
}

// ✅ Asynchronous Processing
for _, email := range emails {
    emailQueue.Enqueue(email)
}

Performance optimization is an ongoing process. Regular monitoring, profiling, and load testing help identify bottlenecks and ensure your application scales effectively.