Files
nmon/internal/api/graph_plan.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

120 lines
3.7 KiB
Go

package api
import (
"errors"
"fmt"
"time"
)
const (
// maxGraphPoints bounds the points per series; the metric store caps a range query near 30,000.
maxGraphPoints = 11_000
// minRawStep is the default floor of a raw step: one poll interval.
minRawStep = 60 * time.Second
// targetPoints is how many points the default step aims for.
targetPoints = 1000
// longStepMin is the resolution of the long store's hourly rollup.
longStepMin = time.Hour
)
var errTooManyPoints = errors.New("too many points")
// tierWindow is one stretch of a graph answered by one store: points at
// from, from+step, ... up to and including last. bound is where the tier
// officially ends (the join for the older tier of a stitched graph).
type tierWindow struct {
tier string
from time.Time
last time.Time
bound time.Time
step time.Duration
}
// graphPlan says which stores answer which part of [from, to]. Tiers are oldest first.
type graphPlan struct {
from, to time.Time // aligned bounds of the whole graph
tiers []tierWindow
stitchedAt *time.Time
}
func (p *graphPlan) newest() tierWindow { return p.tiers[len(p.tiers)-1] }
// floorTo rounds t down to a multiple of d, counted from the Unix epoch so that
// panning a graph never shifts its sample times.
func floorTo(t time.Time, d time.Duration) time.Time {
ds := int64(d / time.Second)
return time.Unix(floorDiv(t.Unix(), ds)*ds, 0).UTC()
}
func ceilTo(t time.Time, d time.Duration) time.Time {
f := floorTo(t, d)
if f.Equal(t) {
return f
}
return f.Add(d)
}
func floorDiv(a, b int64) int64 {
q := a / b
if a%b != 0 && (a < 0) != (b < 0) {
q--
}
return q
}
// defaultStep is max(60 s, range/1000) rounded up to whole minutes, so the
// same range always lands on the same grid.
func defaultStep(rng time.Duration) time.Duration {
return max(minRawStep, ceilDur(rng/targetPoints, minRawStep))
}
func ceilDur(d, unit time.Duration) time.Duration {
return (d + unit - 1) / unit * unit
}
// planGraph decides the tiers for [from, to] at the given step (0: default).
// hotStart is where the hot store's retention begins. A range that lies within
// hot (give or take one step) is answered by raw data alone, a range wholly
// older than hot by hourly data alone, and one that straddles hotStart is
// stitched: hourly before the join, raw from it.
func planGraph(from, to, hotStart time.Time, step time.Duration) (*graphPlan, error) {
if step <= 0 {
step = defaultStep(to.Sub(from))
}
if n := to.Sub(from) / step; n > maxGraphPoints {
return nil, fmt.Errorf("%w: %d points at a %s step; the limit is %d, use a larger step", errTooManyPoints, n, step, maxGraphPoints)
}
fromRaw, toRaw := floorTo(from, step), floorTo(to, step)
long := max(longStepMin, ceilDur(step, longStepMin))
switch {
case !fromRaw.Add(step).Before(hotStart):
return &graphPlan{from: fromRaw, to: toRaw, tiers: []tierWindow{
{tier: tierRaw, from: fromRaw, last: toRaw, bound: toRaw, step: step},
}}, nil
case !toRaw.After(hotStart):
fl, tl := floorTo(from, long), floorTo(to, long)
return &graphPlan{from: fl, to: tl, tiers: []tierWindow{
{tier: tier1h, from: fl, last: tl, bound: tl, step: long},
}}, nil
}
join := ceilTo(hotStart, step)
fl := floorTo(from, long)
return &graphPlan{from: fl, to: toRaw, stitchedAt: &join, tiers: []tierWindow{
{tier: tier1h, from: fl, last: floorTo(join.Add(-time.Nanosecond), long), bound: join, step: long},
{tier: tierRaw, from: join, last: toRaw, bound: toRaw, step: step},
}}, nil
}
// slots lists the sample times of a tier.
func (w tierWindow) slots() []int64 {
if w.last.Before(w.from) {
return nil
}
n := int(w.last.Sub(w.from)/w.step) + 1
out := make([]int64, 0, n)
for t := w.from; !t.After(w.last); t = t.Add(w.step) {
out = append(out, t.Unix())
}
return out
}