разбор метрик HAE: фикстуры, канонизация, парсер
- tmp/research/fixtures.py собирает фикстуры из архива, вычищая измерения и сохраняя порядок ключей, форму литералов, выравнивание меток и невидимые символы; шесть фикстур в internal/hae/testdata - internal/canon — общий дом канонической формы, полноты и хеша: числа читаются литералом через json.Number, округление до 12 значащих цифр - internal/hae — разбор секции metrics, вывод слоя по метрике, разделение схем сна, координаты интервалом; recover внутри Parse, фаззинг - миграция bucket и docs/database.md
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
package hae_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/hae"
|
||||
)
|
||||
|
||||
// Тела доходили до 42 МиБ, и стратегия декодирования — часть контракта, а не
|
||||
// деталь: разбор такого тела в map[string]any удерживает 197 МиБ кучи против
|
||||
// 54 МиБ у формы с json.RawMessage. Вместе с самим телом и удвоением в чтении
|
||||
// пик доходил бы до ~300 МиБ на доставку — это OOM ровно на пике потока, когда
|
||||
// терять доставки дороже всего.
|
||||
//
|
||||
// Тест сторожит именно порядок величины, а не точное число: он обязан падать
|
||||
// при возврате к map[string]any и не падать от смены версии Go.
|
||||
func TestParseУдержаниеКучи(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("измерение кучи: не для -short")
|
||||
}
|
||||
|
||||
body := largeBody(40, 4000)
|
||||
t.Logf("тело %d МиБ", len(body)>>20)
|
||||
|
||||
var before, after runtime.MemStats
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&before)
|
||||
|
||||
res, err := hae.Parse(body, hae.Meta{Aggregation: "Minutes"})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор: %v", err)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
retained := int64(after.HeapAlloc) - int64(before.HeapAlloc)
|
||||
limit := int64(len(body)) * 4
|
||||
|
||||
t.Logf("точек %d, удержано %d МиБ при теле %d МиБ",
|
||||
len(res.Points), retained>>20, len(body)>>20)
|
||||
|
||||
if retained > limit {
|
||||
t.Errorf("удержано %d МиБ при теле %d МиБ — больше четырёх тел; "+
|
||||
"похоже, точки снова разбираются в map[string]any",
|
||||
retained>>20, len(body)>>20)
|
||||
}
|
||||
|
||||
// Результат обязан дожить до замера, иначе мерить нечего.
|
||||
runtime.KeepAlive(res)
|
||||
runtime.KeepAlive(body)
|
||||
}
|
||||
|
||||
// largeBody собирает тело, похожее на настоящее: те же поля, тот же формат
|
||||
// метки, минутное выравнивание.
|
||||
func largeBody(metrics, points int) []byte {
|
||||
var b strings.Builder
|
||||
b.WriteString(`{"data":{"metrics":[`)
|
||||
|
||||
base := time.Date(2025, 6, 5, 0, 0, 0, 0, time.FixedZone("", 3*3600))
|
||||
for m := range metrics {
|
||||
if m > 0 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
fmt.Fprintf(&b, `{"name":"metric_%d","units":"count","data":[`, m)
|
||||
for p := range points {
|
||||
if p > 0 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
ts := base.Add(time.Duration(p) * time.Minute).Format("2006-01-02 15:04:05 -0700")
|
||||
fmt.Fprintf(&b,
|
||||
`{"date":"%s","qty":%d.09523182962471353,"source":"Device A|Device B"}`,
|
||||
ts, p%97)
|
||||
}
|
||||
b.WriteString(`]}`)
|
||||
}
|
||||
|
||||
b.WriteString(`]}}`)
|
||||
return []byte(b.String())
|
||||
}
|
||||
Reference in New Issue
Block a user