Performance Optimization

8 minute read

Performance Overview

Provider performance directly impacts user experience. Slow operations lead to frustration, increased costs, and reduced productivity.

Performance Metrics

Key Indicators

Response Time:
├── API latency
├── Network overhead
├── Processing time
└── State operations

Resource Usage:
├── Memory consumption
├── CPU utilization
├── Goroutine count
└── Network bandwidth

Operation Count:
├── API calls per operation
├── State reads/writes
├── Validation checks
└── Computed attributes

API Call Optimization

Problem: Excessive API Calls

// ❌ Bad: Individual API calls
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var state CacheResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    // Multiple API calls
    cache, err := r.client.GetCache(ctx, state.ID.ValueString())
    if err != nil {
        resp.Diagnostics.AddError("API Error", err.Error())
        return
    }
    
    // Separate call for metrics
    metrics, err := r.client.GetCacheMetrics(ctx, state.ID.ValueString())
    if err != nil {
        resp.Diagnostics.AddError("API Error", err.Error())
        return
    }
    
    // Another call for configuration
    config, err := r.client.GetCacheConfig(ctx, state.ID.ValueString())
    if err != nil {
        resp.Diagnostics.AddError("API Error", err.Error())
        return
    }
    
    // Total: 3 API calls
}

Solution: Batch API Calls

// ✅ Good: Single batch call
func (r *CacheResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var state CacheResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    // Single batch call
    details, err := r.client.GetCacheDetails(ctx, state.ID.ValueString(), &GetOptions{
        IncludeMetrics:       true,
        IncludeConfiguration: true,
    })
    if err != nil {
        resp.Diagnostics.AddError("API Error", err.Error())
        return
    }
    
    // Total: 1 API call (saves ~66% time)
}

Batch Client Implementation

type Client struct {
    httpClient *http.Client
    baseURL    string
}

type GetOptions struct {
    IncludeMetrics       bool
    IncludeConfiguration bool
    IncludeTags          bool
}

func (c *Client) GetCacheDetails(ctx context.Context, id string, opts *GetOptions) (*CacheDetails, error) {
    // Build query parameters
    params := url.Values{}
    if opts.IncludeMetrics {
        params.Add("include", "metrics")
    }
    if opts.IncludeConfiguration {
        params.Add("include", "configuration")
    }
    if opts.IncludeTags {
        params.Add("include", "tags")
    }
    
    // Single request with all data
    endpoint := fmt.Sprintf("%s/caches/%s?%s", c.baseURL, id, params.Encode())
    
    req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var details CacheDetails
    if err := json.NewDecoder(resp.Body).Decode(&details); err != nil {
        return nil, err
    }
    
    return &details, nil
}

Response Caching

In-Memory Cache

type CachedClient struct {
    client *Client
    cache  *sync.Map
    ttl    time.Duration
}

type CacheEntry struct {
    Data      interface{}
    ExpiresAt time.Time
}

func NewCachedClient(client *Client, ttl time.Duration) *CachedClient {
    return &CachedClient{
        client: client,
        cache:  &sync.Map{},
        ttl:    ttl,
    }
}

func (c *CachedClient) GetCache(ctx context.Context, id string) (*Cache, error) {
    cacheKey := fmt.Sprintf("cache:%s", id)
    
    // Check cache
    if entry, found := c.cache.Load(cacheKey); found {
        cached := entry.(*CacheEntry)
        if time.Now().Before(cached.ExpiresAt) {
            tflog.Debug(ctx, "Cache hit", map[string]interface{}{
                "key": cacheKey,
            })
            return cached.Data.(*Cache), nil
        }
        // Expired, remove
        c.cache.Delete(cacheKey)
    }
    
    // Fetch from API
    tflog.Debug(ctx, "Cache miss", map[string]interface{}{
        "key": cacheKey,
    })
    cache, err := c.client.GetCache(ctx, id)
    if err != nil {
        return nil, err
    }
    
    // Store in cache
    c.cache.Store(cacheKey, &CacheEntry{
        Data:      cache,
        ExpiresAt: time.Now().Add(c.ttl),
    })
    
    return cache, nil
}

// Clear cache when modifications occur
func (c *CachedClient) UpdateCache(ctx context.Context, id string, updates *CacheUpdates) (*Cache, error) {
    cache, err := c.client.UpdateCache(ctx, id, updates)
    if err != nil {
        return nil, err
    }
    
    // Invalidate cache
    cacheKey := fmt.Sprintf("cache:%s", id)
    c.cache.Delete(cacheKey)
    
    return cache, nil
}

Cache Configuration

func (p *provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var config providerModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    
    // Create base client
    baseClient := NewClient(config.APIKey.ValueString())
    
    // Wrap with caching (5 minute TTL)
    cachedClient := NewCachedClient(baseClient, 5*time.Minute)
    
    // Make available to resources
    resp.ResourceData = cachedClient
    resp.DataSourceData = cachedClient
}

Parallel Operations

Concurrent API Calls

// ✅ Good: Parallel fetching
func (d *CachesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    var config CachesDataSourceModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
    
    // Get list of cache IDs
    cacheIDs, err := d.client.ListCacheIDs(ctx)
    if err != nil {
        resp.Diagnostics.AddError("API Error", err.Error())
        return
    }
    
    // Fetch details in parallel
    type result struct {
        cache *Cache
        err   error
    }
    
    results := make(chan result, len(cacheIDs))
    var wg sync.WaitGroup
    
    // Limit concurrency
    semaphore := make(chan struct{}, 10) // Max 10 concurrent requests
    
    for _, id := range cacheIDs {
        wg.Add(1)
        go func(cacheID string) {
            defer wg.Done()
            
            semaphore <- struct{}{}        // Acquire
            defer func() { <-semaphore }() // Release
            
            cache, err := d.client.GetCache(ctx, cacheID)
            results <- result{cache: cache, err: err}
        }(id)
    }
    
    // Wait for completion
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Collect results
    var caches []*Cache
    for res := range results {
        if res.err != nil {
            resp.Diagnostics.AddError("API Error", res.err.Error())
            continue
        }
        caches = append(caches, res.cache)
    }
    
    // Build state
    state := CachesDataSourceModel{
        Caches: buildCacheModels(ctx, caches),
    }
    resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

Error Handling with Parallel Operations

func fetchParallel(ctx context.Context, client *Client, ids []string) ([]*Cache, error) {
    type result struct {
        index int
        cache *Cache
        err   error
    }
    
    results := make(chan result, len(ids))
    var wg sync.WaitGroup
    
    for i, id := range ids {
        wg.Add(1)
        go func(index int, cacheID string) {
            defer wg.Done()
            
            cache, err := client.GetCache(ctx, cacheID)
            results <- result{
                index: index,
                cache: cache,
                err:   err,
            }
        }(i, id)
    }
    
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Collect results preserving order
    caches := make([]*Cache, len(ids))
    var errs []error
    
    for res := range results {
        if res.err != nil {
            errs = append(errs, fmt.Errorf("cache %s: %w", ids[res.index], res.err))
            continue
        }
        caches[res.index] = res.cache
    }
    
    if len(errs) > 0 {
        return nil, fmt.Errorf("failed to fetch caches: %v", errs)
    }
    
    return caches, nil
}

Connection Pooling

HTTP Client Configuration

func NewOptimizedClient(apiKey string) *Client {
    // Configure transport with pooling
    transport := &http.Transport{
        MaxIdleConns:        100,              // Total idle connections
        MaxIdleConnsPerHost: 10,               // Idle per host
        MaxConnsPerHost:     50,               // Max per host
        IdleConnTimeout:     90 * time.Second, // Idle timeout
        TLSHandshakeTimeout: 10 * time.Second,
        DisableCompression:  false,
        DisableKeepAlives:   false, // Enable keep-alive
    }
    
    httpClient := &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
    }
    
    return &Client{
        httpClient: httpClient,
        apiKey:     apiKey,
    }
}

Connection Reuse

// ✅ Good: Reuse connections
func (c *Client) makeRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
    req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("Authorization", "Bearer "+c.apiKey)
    req.Header.Set("User-Agent", "terraform-provider-cachecloud/1.0")
    
    // Connection pooling happens automatically
    return c.httpClient.Do(req)
}

Memory Optimization

Efficient State Handling

// ❌ Bad: Copying entire state
func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var plan, state CacheResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    // Full copy of potentially large state
    newState := state
    newState.Name = plan.Name
    newState.Config = plan.Config
    // ...
}

// ✅ Good: Modify in place
func (r *CacheResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var plan, state CacheResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
    resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
    
    // Only update changed fields
    if !plan.Name.Equal(state.Name) {
        state.Name = plan.Name
    }
    if !plan.Config.Equal(state.Config) {
        state.Config = plan.Config
    }
    
    resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

Stream Large Responses

// ✅ Good: Stream and process incrementally
func (c *Client) ExportLargeDataset(ctx context.Context, id string) error {
    resp, err := c.httpClient.Get(c.baseURL + "/export/" + id)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    // Stream process without loading entire response
    scanner := bufio.NewScanner(resp.Body)
    scanner.Buffer(make([]byte, 64*1024), 1024*1024) // 1MB buffer
    
    for scanner.Scan() {
        line := scanner.Bytes()
        // Process line incrementally
        if err := processLine(line); err != nil {
            return err
        }
    }
    
    return scanner.Err()
}

Computed Attribute Optimization

Selective Computation

schema.Int64Attribute{
    Description: "Total size in bytes",
    Computed:    true,
    // Only recompute when these attributes change
    PlanModifiers: []planmodifier.Int64{
        int64planmodifier.UseStateForUnknown(), // Skip if plan is unknown
    },
}

Efficient Default Values

// ✅ Good: Static default
schema.StringAttribute{
    Optional: true,
    Computed: true,
    Default:  stringdefault.StaticString("cache.t3.micro"),
}

// ❌ Bad: Dynamic default requiring computation
schema.StringAttribute{
    Optional: true,
    Computed: true,
    Default:  stringdefault.UseStateForUnknown(), // Recomputed every time
}

Benchmark Testing

Basic Benchmark

func BenchmarkCacheRead(b *testing.B) {
    ctx := context.Background()
    client := NewClient("test-key")
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := client.GetCache(ctx, "test-cache-id")
        if err != nil {
            b.Fatal(err)
        }
    }
}

Parallel Benchmark

func BenchmarkCacheReadParallel(b *testing.B) {
    ctx := context.Background()
    client := NewClient("test-key")
    
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            _, err := client.GetCache(ctx, "test-cache-id")
            if err != nil {
                b.Fatal(err)
            }
        }
    })
}

Memory Allocation Benchmark

func BenchmarkStateUpdate(b *testing.B) {
    ctx := context.Background()
    
    b.ReportAllocs() // Report memory allocations
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        state := CacheResourceModel{
            ID:   types.StringValue("test-id"),
            Name: types.StringValue("test-name"),
        }
        
        // Simulate state update
        state.Name = types.StringValue("new-name")
    }
}

Running Benchmarks

# Run all benchmarks
go test -bench=. -benchmem

# Output:
# BenchmarkCacheRead-8                1000    1234567 ns/op    4096 B/op    10 allocs/op
# BenchmarkCacheReadParallel-8        5000     246813 ns/op    4096 B/op    10 allocs/op
# BenchmarkStateUpdate-8          10000000       123 ns/op      64 B/op     2 allocs/op

# Run specific benchmark
go test -bench=BenchmarkCacheRead -benchtime=10s

# CPU profiling
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof

# Memory profiling
go test -bench=. -memprofile=mem.prof
go tool pprof mem.prof

Profiling Production Code

CPU Profiling

import (
    "os"
    "runtime/pprof"
)

func profileCPU() func() {
    f, err := os.Create("cpu.prof")
    if err != nil {
        log.Fatal(err)
    }
    
    pprof.StartCPUProfile(f)
    
    return func() {
        pprof.StopCPUProfile()
        f.Close()
    }
}

// Usage in tests
func TestComplexOperation(t *testing.T) {
    stop := profileCPU()
    defer stop()
    
    // Run operation
    complexOperation()
}

Memory Profiling

func profileMemory() {
    f, err := os.Create("mem.prof")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    
    runtime.GC() // Force GC
    if err := pprof.WriteHeapProfile(f); err != nil {
        log.Fatal(err)
    }
}

Monitoring Performance

Request Duration Metrics

type MetricsMiddleware struct {
    next http.RoundTripper
}

func (m *MetricsMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
    start := time.Now()
    
    resp, err := m.next.RoundTrip(req)
    
    duration := time.Since(start)
    tflog.Debug(req.Context(), "API request completed",
        map[string]interface{}{
            "method":      req.Method,
            "url":         req.URL.String(),
            "duration_ms": duration.Milliseconds(),
            "status":      resp.StatusCode,
        })
    
    return resp, err
}

Performance Logging

func (r *CacheResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    start := time.Now()
    defer func() {
        tflog.Debug(ctx, "Create operation completed",
            map[string]interface{}{
                "duration_ms": time.Since(start).Milliseconds(),
            })
    }()
    
    // Implementation...
}

Best Practices

1. Batch Operations

// ✅ Good: Single batch call
items, err := client.BatchGet(ctx, ids)

// ❌ Bad: Individual calls in loop
for _, id := range ids {
    item, err := client.Get(ctx, id)
}

2. Use Caching Wisely

// ✅ Good: Cache immutable data
cachedClient.GetAccountInfo(ctx) // Account info rarely changes

// ❌ Bad: Cache frequently changing data
cachedClient.GetCurrentMetrics(ctx) // Metrics change constantly

3. Limit Concurrency

// ✅ Good: Controlled concurrency
semaphore := make(chan struct{}, 10) // Max 10 concurrent

// ❌ Bad: Unbounded concurrency
for _, id := range thousandsOfIDs {
    go fetch(id) // Can overwhelm system
}

4. Profile Before Optimizing

# ✅ Good: Measure first
go test -bench=. -cpuprofile=cpu.prof
# Identify bottlenecks
# Then optimize

# ❌ Bad: Premature optimization
# Optimize without measuring

Key Takeaways

  • ✅ Batch API calls to reduce latency
  • ✅ Implement caching for immutable data
  • ✅ Use parallel operations with limits
  • ✅ Configure connection pooling
  • ✅ Optimize memory usage
  • ✅ Benchmark critical paths
  • ✅ Profile to find bottlenecks
  • ✅ Monitor performance in production

What’s Next?

Learn advanced state management including migrations and upgrades.

Next: Advanced State Management →


Previous: ← Complex Attribute Types
Next: Advanced State Management →

Last updated: July 1, 2026

Updated: