package points_test import ( "context" "encoding/json" "log/slog" "os" "path/filepath" "runtime" "testing" "time" "git.vakhrushev.me/av/healthlog/internal/hae" "git.vakhrushev.me/av/healthlog/internal/points" "git.vakhrushev.me/av/healthlog/internal/store" ) // ЦЕНА МАРШРУТА ТОЧЕК, ЗАМЕР. // // Предела размера ответа у маршрута нет намеренно — его вводит соседняя задача // `read-api-response-limit`. Поэтому цена обязана быть НАЗВАНА ЧИСЛОМ, и число // снимается на том режиме, ради которого предел заводится, а не на том, который // оказался под рукой: прецедент 2026-08-04 (`docs/review.md`) — замер, снятый на // корпусе, где измеряемого случая не бывает, стоил решения «индекс не нужен». // // Три режима, от лёгкого к худшему: // // редкая метрика за год — «вес за год», сценарий постановки; // плотная метрика за сутки — минутный слой, штатный запрос трекера; // плотная метрика за неделю в нижнем слое — то, что правило выбора слоя // отдаёт по умолчанию, когда охваты равны. // // Точки размножаются из РЕАЛЬНОГО пакета HAE (`internal/hae/testdata`), а не // пишутся литералами: форма точки, длина строк и вид числового литерала входят // в цену — они определяют и объём gzip, и работу разжатия. // // Прогон: go test ./internal/points -run XXX -bench . -benchtime 1x func BenchmarkРядТочек(b *testing.B) { cases := []struct { name string layer hae.Layer hours int perDay int }{ // Год, взвешивание примерно раз в сутки. {"редкая метрика за год", hae.LayerHour, 365 * 24, 1}, // Сутки минутного слоя: 60 точек в час. {"плотная метрика за сутки, minute", hae.LayerMinute, 24, 24 * 60}, // Неделя нижнего слоя. Порядок взят из разведки: около 100 тысяч // координат в сутки на весь поток; на одну плотную метрику — 3600 в час. {"плотная метрика за неделю, raw", hae.LayerRaw, 7 * 24, 24 * 3600}, } for _, c := range cases { b.Run(c.name, func(b *testing.B) { svc, from, to := benchStore(b, c.layer, c.hours, c.perDay) var before, after runtime.MemStats runtime.GC() runtime.ReadMemStats(&before) b.ResetTimer() var got points.Series for range b.N { var err error got, err = svc.Series(context.Background(), points.Request{ Metric: "bench_metric", From: from, To: to, Layer: string(c.layer), }) if err != nil { b.Fatalf("Series: %v", err) } } b.StopTimer() runtime.ReadMemStats(&after) b.ReportMetric(float64(len(got.Points)), "точек") b.ReportMetric(float64(after.TotalAlloc-before.TotalAlloc)/(1<<20)/float64(b.N), "МиБ_выделено") }) } } // benchStore наполняет витрину точками, размноженными из реального пакета HAE. func benchStore(b *testing.B, layer hae.Layer, hours, perDay int) (*points.Service, time.Time, time.Time) { b.Helper() raw := realPointBody(b) st, err := store.Open(filepath.Join(b.TempDir(), "healthlog.db")) if err != nil { b.Fatalf("store.Open: %v", err) } b.Cleanup(func() { _ = st.Close() }) perHour := max(perDay/24, 1) step := time.Hour / time.Duration(perHour) start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) ctx := context.Background() // Пачками по часу: одна транзакция на весь год держала бы блокировку минуты. for h := range hours { hour := start.Add(time.Duration(h) * time.Hour) in := make([]store.IncomingPoint, 0, perHour) for i := range perHour { at := hour.Add(time.Duration(i) * step) in = append(in, store.IncomingPoint{ Metric: "bench_metric", Layer: string(layer), Units: "count", Point: store.Point{Start: at, End: at, OffsetSeconds: 10800, Raw: raw}, }) } if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "bench"}); err != nil { b.Fatalf("слияние: %v", err) } } svc := points.New(st, slog.New(slog.DiscardHandler)) return svc, start, start.Add(time.Duration(hours) * time.Hour) } // realPointBody достаёт содержимое настоящей точки из пакета HAE. // // Конвенция проекта: тесты формата держим на реальных пакетах. Здесь она нужна // не ради разбора, а ради ЦЕНЫ — выдуманная точка `{"qty":1}` жмётся иначе и // разжимается быстрее, чем то, что реально шлёт телефон. func realPointBody(b *testing.B) json.RawMessage { b.Helper() body, err := os.ReadFile(filepath.Join("..", "hae", "testdata", "minute.json")) if err != nil { b.Fatalf("чтение пакета: %v", err) } var pkg struct { Data struct { Metrics []struct { Data []json.RawMessage `json:"data"` } `json:"metrics"` } `json:"data"` } if err := json.Unmarshal(body, &pkg); err != nil { b.Fatalf("разбор пакета: %v", err) } for _, m := range pkg.Data.Metrics { if len(m.Data) > 0 { return m.Data[0] } } b.Fatal("в пакете нет ни одной точки") return nil }