Files
nmon/internal/api/graph_series.go
jleung f85c3ef781
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
M3: Go API per UI contract, embedded UI, openapi compat check, nmon-devtools, live gate + screenshots; M4 WP1 reachability and dependency suppression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 20:34:14 +00:00

81 lines
2.2 KiB
Go

package api
import (
"math"
"sort"
"time"
)
// gapFactor is how many steps without data make a stretch "partial".
const gapFactor = 2
// slot is one sample time of the graph and the step of the tier it belongs to.
type slot struct {
ts int64
step int64
}
// gridOf lists every sample time of the plan, oldest first.
func gridOf(p *graphPlan) []slot {
var out []slot
for _, w := range p.tiers {
step := int64(w.step / time.Second)
for _, ts := range w.slots() {
out = append(out, slot{ts: ts, step: step})
}
}
return out
}
// pointsOf lays vals over the grid; a slot without data is a null point.
func pointsOf(grid []slot, vals map[int64]float64) []Point {
out := make([]Point, len(grid))
for i, g := range grid {
out[i] = Point{TS: g.ts}
if v, ok := vals[g.ts]; ok {
x := v
out[i].V = &x
}
}
return out
}
// findGaps returns the stretches of [start, end] longer than gapFactor steps in
// which no slot has data. A stretch runs from the last slot with data (or the
// start) to the next one (or the end); its step is the larger of its two
// neighbours', so a join between tiers does not report a false gap.
func findGaps(grid []slot, has func(ts int64) bool, start, end int64) []TimeSpan {
if len(grid) == 0 {
return nil
}
var gaps []TimeSpan
prevTS, prevStep := start, grid[0].step
report := func(from, to, step int64) {
if to-from > gapFactor*step {
gaps = append(gaps, TimeSpan{From: time.Unix(from, 0).UTC(), To: time.Unix(to, 0).UTC()})
}
}
for _, g := range grid {
if !has(g.ts) {
continue
}
report(prevTS, g.ts, max(prevStep, g.step))
prevTS, prevStep = g.ts, g.step
}
report(prevTS, end, prevStep)
return gaps
}
// nearestRank is the p-quantile (0 < p <= 1) of vals by the nearest-rank
// method: the value at rank ceil(p*n) of the sorted list. It is exact, takes
// an actual sample and needs no interpolation convention. NaN for no values.
func nearestRank(vals []float64, p float64) float64 {
if len(vals) == 0 {
return math.NaN()
}
s := append([]float64(nil), vals...)
sort.Float64s(s)
rank := int(math.Ceil(p*float64(len(s)) - 1e-9)) // the epsilon keeps 0.95*20 from rounding up to 20
return s[min(max(rank, 1), len(s))-1]
}