fintual-backend-devops-go / harness / compare.go
compare.go
Raw
package main

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"sort"
)

type MetricComparison struct {
	Metric        string  `json:"metric"`
	BaselineMean  float64 `json:"baseline_mean"`
	CandidateMean float64 `json:"candidate_mean"`
	Ratio         float64 `json:"candidate_over_baseline"`
	Low           float64 `json:"difference_ci_low"`
	High          float64 `json:"difference_ci_high"`
	Pass          bool    `json:"pass"`
	Note          string  `json:"note,omitempty"`
}
type WorkloadComparison struct {
	Workload    string             `json:"workload"`
	Concurrency int                `json:"concurrency"`
	Pass        bool               `json:"pass"`
	Metrics     []MetricComparison `json:"metrics"`
}
type Comparison struct {
	Baseline  string               `json:"baseline"`
	Candidate string               `json:"candidate"`
	Pass      bool                 `json:"pass"`
	Workloads []WorkloadComparison `json:"workloads"`
}

func mean(v []float64) float64 {
	sum := 0.
	for _, x := range v {
		sum += x
	}
	return sum / float64(len(v))
}
func bootstrap(a, b []float64) (float64, float64) {
	rng := rand.New(rand.NewSource(42))
	values := make([]float64, 10000)
	for i := range values {
		av, bv := 0., 0.
		for range a {
			av += a[rng.Intn(len(a))]
		}
		for range b {
			bv += b[rng.Intn(len(b))]
		}
		values[i] = bv/float64(len(b)) - av/float64(len(a))
	}
	sort.Float64s(values)
	return values[250], values[9749]
}
func compareFiles(baseline, candidate, output, filter string) error {
	load := func(path string) (BenchReport, error) {
		raw, err := os.ReadFile(path)
		if err != nil {
			return BenchReport{}, err
		}
		var report BenchReport
		err = json.Unmarshal(raw, &report)
		return report, err
	}
	a, err := load(baseline)
	if err != nil {
		return err
	}
	b, err := load(candidate)
	if err != nil {
		return err
	}
	if a.Dataset != b.Dataset {
		return fmt.Errorf("dataset mismatch")
	}
	if a.Preparation != b.Preparation {
		return fmt.Errorf("database preparation mismatch")
	}
	am, bm := map[string][]Sample{}, map[string][]Sample{}
	for _, s := range a.Samples {
		key := fmt.Sprintf("%s/%d", s.Workload, s.Concurrency)
		am[key] = append(am[key], s)
	}
	for _, s := range b.Samples {
		key := fmt.Sprintf("%s/%d", s.Workload, s.Concurrency)
		bm[key] = append(bm[key], s)
	}
	keys := []string{}
	for k := range am {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	result := Comparison{Baseline: baseline, Candidate: candidate, Pass: true, Workloads: []WorkloadComparison{}}
	metrics := []struct {
		name    string
		larger  bool
		extract func(Sample) float64
	}{
		{"p50_ms", false, func(s Sample) float64 { return s.P50MS }},
		{"p95_ms", false, func(s Sample) float64 { return s.P95MS }},
		{"rps", true, func(s Sample) float64 { return s.RPS }},
		{"cpu_ms_per_request", false, func(s Sample) float64 {
			if s.Requests == s.Errors {
				return 0
			}
			return float64(s.After.CPUTicks-s.Before.CPUTicks) * 10 / float64(s.Requests-s.Errors)
		}},
		{"peak_pss_kb", false, func(s Sample) float64 { return float64(s.Peak.PSSKB) }},
		{"peak_rss_kb", false, func(s Sample) float64 { return float64(s.Peak.RSSKB) }},
	}
	for _, key := range keys {
		x, y := am[key], bm[key]
		if filter != "" && x[0].Workload != filter {
			continue
		}
		if len(x) != len(y) || len(x) < 5 {
			return fmt.Errorf("need matching >=5 repetitions for %s", key)
		}
		w := WorkloadComparison{Workload: x[0].Workload, Concurrency: x[0].Concurrency, Pass: true, Metrics: []MetricComparison{}}
		for i := range x {
			if x[i].Repeat != y[i].Repeat || x[i].Concurrency != y[i].Concurrency {
				return fmt.Errorf("repetition mismatch %s", key)
			}
			if x[i].Requests != y[i].Requests {
				return fmt.Errorf("request count mismatch %s", key)
			}
			if x[i].Errors != 0 || y[i].Errors != 0 {
				w.Pass = false
			}
		}
		for _, metric := range metrics {
			av, bv := []float64{}, []float64{}
			for i := range x {
				av = append(av, metric.extract(x[i]))
				bv = append(bv, metric.extract(y[i]))
			}
			lo, hi := bootstrap(av, bv)
			m := MetricComparison{Metric: metric.name, BaselineMean: mean(av), CandidateMean: mean(bv), Low: lo, High: hi, Pass: hi <= 0}
			if m.BaselineMean != 0 {
				m.Ratio = m.CandidateMean / m.BaselineMean
			}
			if metric.larger {
				m.Pass = lo >= 0
			}
			if metric.name == "cpu_ms_per_request" && (m.BaselineMean == 0 || m.CandidateMean == 0) {
				m.Pass = false
				m.Note = "CPU tick resolution insufficient; increase request count"
			}
			if x[0].Requests < 20 && (metric.name == "p50_ms" || metric.name == "p95_ms") {
				m.Note = "low-sample full-response duration; not a reliable tail estimate"
			}
			w.Pass = w.Pass && m.Pass
			w.Metrics = append(w.Metrics, m)
		}
		result.Pass = result.Pass && w.Pass
		result.Workloads = append(result.Workloads, w)
		fmt.Printf("%s pass=%v\n", key, w.Pass)
	}
	if len(bm) != len(am) {
		return fmt.Errorf("candidate workload set differs")
	}
	if len(result.Workloads) == 0 {
		return fmt.Errorf("no matching workload")
	}
	if err = os.MkdirAll(filepath.Dir(output), 0755); err != nil {
		return err
	}
	raw, _ := json.MarshalIndent(result, "", "  ")
	if err = os.WriteFile(output, append(raw, '\n'), 0644); err != nil {
		return err
	}
	if !result.Pass {
		return fmt.Errorf("comparison contains failing or inconclusive gates; see %s", output)
	}
	return nil
}