Cap retained chart observations before allocating wire points

Share the existing time-nearest selection by original indices, so chart conversion materialises only selected observations. Preserve exact timestamps, values, tie breaks, degenerate spans and oldest-source metadata. Check the frozen former sampler across 1,440 cases and the exported fallback contract.

Change-source: pulse-maintainer
Contract-Neutral: Exact chart observations, wire schema, sampling policy and guest/storage lifecycle contracts are unchanged; only index selection and allocation order change.
This commit is contained in:
pulse-triage[bot]
2026-10-24 14:08:19 +01:00
parent c36672b2a7
commit bf9f0ed982
5 changed files with 356 additions and 97 deletions
@@ -818,6 +818,25 @@ executes a terminal-free Unix recorder and inspects explicit Windows parameter
binding. Browser evidence is UI-only; server/native deregistration and Windows
failure retention require their own owner result, not source acceptance.
### Chart caps select before wire conversion (24 October 2026)
The existing time-nearest chart cap selects indices in the original series
before allocating wire-format points. Its equal-time targets, latest-time tie
break, first/last observations and degenerate-span index fallback are unchanged.
No average, synthetic timestamp or new sampling policy is introduced; zero and
selected extrema retain their exact values. Oldest-data metadata still examines
every eligible source observation, including one omitted from display. Empty
series remain empty and each converted response owns its point slices. The
retained reader, LTTB stage, source/alias choice, scopes and cache budgets stay
unchanged.
`TestChartPointSelectionMatchesOriginalCap` compares the frozen former algorithm
with both wire and source-point paths over mixed cadence, millisecond ties,
negative epochs, unsorted/degenerate spans and caps from unlimited to one point.
`TestContract_ChartIndexFallbackPreservesObservations` checks the exported fallback
with equal-time, fractional, negative and explicit-zero observations. Source
parity and consuming browser evidence remain separate acceptance obligations.
## Canonical Files
1a. `docs/API.md`
@@ -626,6 +626,25 @@ when the row is clicked: no document listener, timer, signal, metric derivation
or History work is added per row. `GuestRow.test.tsx` covers the drag, the
in-place click and chevron activation over a selection.
### Chart caps select before wire conversion (24 October 2026)
The existing time-nearest chart cap selects indices in the original series
before allocating wire-format points. Its equal-time targets, latest-time tie
break, first/last observations and degenerate-span index fallback are unchanged.
No average, synthetic timestamp or new sampling policy is introduced; zero and
selected extrema retain their exact values. Oldest-data metadata still examines
every eligible source observation, including one omitted from display. Empty
series remain empty and each converted response owns its point slices. The
retained reader, LTTB stage, source/alias choice, scopes and cache budgets stay
unchanged.
`TestChartPointSelectionMatchesOriginalCap` compares the frozen former algorithm
with both wire and source-point paths over mixed cadence, millisecond ties,
negative epochs, unsorted/degenerate spans and caps from unlimited to one point.
`TestContract_ChartIndexFallbackPreservesObservations` checks the exported fallback
with equal-time, fractional, negative and explicit-zero observations. Source
parity and consuming browser evidence remain separate acceptance obligations.
## Canonical Files
- `frontend-modern/src/utils/workloadDiskUsage.ts` — aggregate disk numeric/current-reading admission.
+176
View File
@@ -2,6 +2,10 @@ package chartapi
import (
"encoding/json"
"fmt"
"math"
"math/rand"
"reflect"
"testing"
"time"
@@ -651,3 +655,175 @@ func TestEmptyDockerData(t *testing.T) {
t.Errorf("dockerData should be an empty object, not missing")
}
}
// A frozen copy of the former wire-point cap is the oracle: source-point
// selection must preserve its exact timestamps/values, not just a point count.
func referenceChartCapByIndex(points []MetricPoint, maxPoints int) []MetricPoint {
if len(points) <= maxPoints || maxPoints <= 0 {
return points
}
if maxPoints == 1 {
return []MetricPoint{points[len(points)-1]}
}
result := make([]MetricPoint, 0, maxPoints)
step := float64(len(points)-1) / float64(maxPoints-1)
prevIndex := -1
for i := 0; i < maxPoints; i++ {
index := int(float64(i)*step + 0.5)
if index <= prevIndex {
index = prevIndex + 1
}
if index >= len(points) {
index = len(points) - 1
}
result = append(result, points[index])
prevIndex = index
}
if result[len(result)-1].Timestamp != points[len(points)-1].Timestamp {
result[len(result)-1] = points[len(points)-1]
}
return result
}
func referenceChartCap(points []MetricPoint, maxPoints int) []MetricPoint {
if len(points) <= maxPoints || maxPoints <= 0 {
return points
}
if maxPoints == 1 {
return []MetricPoint{points[len(points)-1]}
}
startTimestamp := points[0].Timestamp
endTimestamp := points[len(points)-1].Timestamp
if endTimestamp <= startTimestamp {
return referenceChartCapByIndex(points, maxPoints)
}
bucketSpan := float64(endTimestamp-startTimestamp) / float64(maxPoints-1)
if bucketSpan < 1 {
return referenceChartCapByIndex(points, maxPoints)
}
type timeBucketRepresentative struct {
point MetricPoint
distance float64
ok bool
}
buckets := make([]timeBucketRepresentative, maxPoints)
for _, point := range points {
index := int(math.Round(float64(point.Timestamp-startTimestamp) / bucketSpan))
if index < 0 {
index = 0
}
if index >= maxPoints {
index = maxPoints - 1
}
targetTimestamp := float64(startTimestamp) + bucketSpan*float64(index)
distance := math.Abs(float64(point.Timestamp) - targetTimestamp)
current := buckets[index]
if !current.ok ||
distance < current.distance ||
(distance == current.distance && point.Timestamp > current.point.Timestamp) {
buckets[index] = timeBucketRepresentative{
point: point,
distance: distance,
ok: true,
}
}
}
result := make([]MetricPoint, 0, maxPoints)
result = append(result, points[0])
lastAddedTimestamp := points[0].Timestamp
for index := 1; index < maxPoints-1; index++ {
bucket := buckets[index]
if !bucket.ok {
continue
}
if bucket.point.Timestamp <= lastAddedTimestamp {
continue
}
result = append(result, bucket.point)
lastAddedTimestamp = bucket.point.Timestamp
}
lastPoint := points[len(points)-1]
if lastPoint.Timestamp <= lastAddedTimestamp {
result[len(result)-1] = lastPoint
return result
}
result = append(result, lastPoint)
return result
}
func TestChartPointSelectionMatchesOriginalCap(t *testing.T) {
t.Parallel()
rng := rand.New(rand.NewSource(20261024))
for scenario := 0; scenario < 160; scenario++ {
n := []int{0, 1, 2, 3, 17, 121, 240, 500, 777}[scenario%9]
points := make([]MetricPoint, n)
stamp := int64(1_800_000_000_123)
if scenario%11 == 0 {
stamp = -100000
}
for i := range points {
step := int64(rng.Intn(4000))
if i > n/2 {
step %= 5 // dense recent tail, duplicate/millisecond timestamps
}
stamp += step
points[i] = MetricPoint{Timestamp: stamp, Value: float64(rng.Intn(201)-100) / 7}
}
if n > 2 {
points[n-1].Value = 0
switch scenario % 5 {
case 0:
for i := range points {
points[i].Timestamp = stamp // no positive span: index fallback
}
case 1:
points[0], points[n-1] = points[n-1], points[0]
case 2:
points[n/2].Timestamp = points[0].Timestamp - 7 // unselected minimum
}
}
for _, cap := range []int{-1, 0, 1, 2, 3, 30, 36, 120, 500} {
t.Run(fmt.Sprintf("%d/cap=%d", scenario, cap), func(t *testing.T) {
want := referenceChartCap(points, cap)
if got := capMetricPointSeries(points, cap); !reflect.DeepEqual(got, want) {
t.Fatal("time cap changed its selected observations")
}
if got, want := capMetricPointSeriesByIndex(points, cap), referenceChartCapByIndex(points, cap); !reflect.DeepEqual(got, want) {
t.Fatal("index fallback changed its selected observations")
}
source := make([]monitoring.MetricPoint, n)
wantOldest := int64(math.MaxInt64)
for i, point := range points {
source[i] = monitoring.MetricPoint{Timestamp: time.UnixMilli(point.Timestamp), Value: point.Value}
if point.Timestamp < wantOldest {
wantOldest = point.Timestamp
}
}
oldest := int64(math.MaxInt64)
got := convertMetricsForChart(map[string][]monitoring.MetricPoint{
"cpu": source, "not-a-sparkline": {{Timestamp: time.UnixMilli(math.MinInt64), Value: -999}},
}, &oldest, cap)
if len(got) != 1 || !reflect.DeepEqual(got["cpu"], want) || oldest != wantOldest {
t.Fatalf("source conversion/cap/oldest changed: oldest %d, want %d", oldest, wantOldest)
}
if len(got["cpu"]) > 0 {
got["cpu"][0].Value = 9999
if source[0].Value != points[0].Value {
t.Fatal("wire output aliases retained source")
}
}
})
}
}
}
+116 -97
View File
@@ -623,28 +623,10 @@ func capMetricPointSeriesByIndex(points []MetricPoint, maxPoints int) []MetricPo
if len(points) <= maxPoints || maxPoints <= 0 {
return points
}
if maxPoints == 1 {
return []MetricPoint{points[len(points)-1]}
}
result := make([]MetricPoint, 0, maxPoints)
step := float64(len(points)-1) / float64(maxPoints-1)
prevIndex := -1
for i := 0; i < maxPoints; i++ {
index := int(float64(i)*step + 0.5)
if index <= prevIndex {
index = prevIndex + 1
}
if index >= len(points) {
index = len(points) - 1
}
result = append(result, points[index])
prevIndex = index
}
if result[len(result)-1].Timestamp != points[len(points)-1].Timestamp {
result[len(result)-1] = points[len(points)-1]
indices := chartPointIndicesByIndex(len(points), maxPoints)
result := make([]MetricPoint, len(indices))
for i, index := range indices {
result[i] = points[index]
}
return result
}
@@ -660,6 +642,89 @@ const (
const InfrastructureSummaryMaxSeriesPoints = infrastructureSummaryMaxSeriesPoints
func chartPointIndicesByIndex(count, maxPoints int) []int {
if maxPoints == 1 {
return []int{count - 1}
}
indices := make([]int, 0, maxPoints)
step := float64(count-1) / float64(maxPoints-1)
prevIndex := -1
for i := 0; i < maxPoints; i++ {
index := int(float64(i)*step + 0.5)
if index <= prevIndex {
index = prevIndex + 1
}
if index >= count {
index = count - 1
}
indices = append(indices, index)
prevIndex = index
}
return indices
}
// chartPointIndices selects original observations, not averages or synthetic
// timestamps. Call only when count > maxPoints > 0. Keeping indices lets the
// retained reader apply the same cap before allocating wire-format points.
func chartPointIndices(count, maxPoints int, timestampAt func(int) int64) []int {
if maxPoints == 1 {
return []int{count - 1}
}
startTimestamp, endTimestamp := timestampAt(0), timestampAt(count-1)
if endTimestamp <= startTimestamp {
return chartPointIndicesByIndex(count, maxPoints)
}
bucketSpan := float64(endTimestamp-startTimestamp) / float64(maxPoints-1)
if bucketSpan < 1 {
return chartPointIndicesByIndex(count, maxPoints)
}
type representative struct {
indexPlusOne int // zero means no observation in this time bucket
distance float64
}
buckets := make([]representative, maxPoints)
for pointIndex := 0; pointIndex < count; pointIndex++ {
timestamp := timestampAt(pointIndex)
index := int(math.Round(float64(timestamp-startTimestamp) / bucketSpan))
if index < 0 {
index = 0
}
if index >= maxPoints {
index = maxPoints - 1
}
targetTimestamp := float64(startTimestamp) + bucketSpan*float64(index)
distance := math.Abs(float64(timestamp) - targetTimestamp)
current := buckets[index]
if current.indexPlusOne == 0 || distance < current.distance ||
(distance == current.distance && timestamp > timestampAt(current.indexPlusOne-1)) {
buckets[index] = representative{indexPlusOne: pointIndex + 1, distance: distance}
}
}
result := make([]int, 0, maxPoints)
result = append(result, 0)
lastAddedTimestamp := startTimestamp
for index := 1; index < maxPoints-1; index++ {
bucket := buckets[index]
if bucket.indexPlusOne == 0 {
continue
}
pointIndex := bucket.indexPlusOne - 1
timestamp := timestampAt(pointIndex)
if timestamp <= lastAddedTimestamp {
continue
}
result = append(result, pointIndex)
lastAddedTimestamp = timestamp
}
if endTimestamp <= lastAddedTimestamp {
result[len(result)-1] = count - 1
return result
}
return append(result, count-1)
}
// capMetricPointSeries keeps mixed-cadence series visually proportional across
// the selected time window. Index-based capping over-selects recent dense
// samples, which bunches the right edge on long ranges.
@@ -667,73 +732,11 @@ func capMetricPointSeries(points []MetricPoint, maxPoints int) []MetricPoint {
if len(points) <= maxPoints || maxPoints <= 0 {
return points
}
if maxPoints == 1 {
return []MetricPoint{points[len(points)-1]}
indices := chartPointIndices(len(points), maxPoints, func(i int) int64 { return points[i].Timestamp })
result := make([]MetricPoint, len(indices))
for i, index := range indices {
result[i] = points[index]
}
startTimestamp := points[0].Timestamp
endTimestamp := points[len(points)-1].Timestamp
if endTimestamp <= startTimestamp {
return capMetricPointSeriesByIndex(points, maxPoints)
}
bucketSpan := float64(endTimestamp-startTimestamp) / float64(maxPoints-1)
if bucketSpan < 1 {
return capMetricPointSeriesByIndex(points, maxPoints)
}
type timeBucketRepresentative struct {
point MetricPoint
distance float64
ok bool
}
buckets := make([]timeBucketRepresentative, maxPoints)
for _, point := range points {
index := int(math.Round(float64(point.Timestamp-startTimestamp) / bucketSpan))
if index < 0 {
index = 0
}
if index >= maxPoints {
index = maxPoints - 1
}
targetTimestamp := float64(startTimestamp) + bucketSpan*float64(index)
distance := math.Abs(float64(point.Timestamp) - targetTimestamp)
current := buckets[index]
if !current.ok ||
distance < current.distance ||
(distance == current.distance && point.Timestamp > current.point.Timestamp) {
buckets[index] = timeBucketRepresentative{
point: point,
distance: distance,
ok: true,
}
}
}
result := make([]MetricPoint, 0, maxPoints)
result = append(result, points[0])
lastAddedTimestamp := points[0].Timestamp
for index := 1; index < maxPoints-1; index++ {
bucket := buckets[index]
if !bucket.ok {
continue
}
if bucket.point.Timestamp <= lastAddedTimestamp {
continue
}
result = append(result, bucket.point)
lastAddedTimestamp = bucket.point.Timestamp
}
lastPoint := points[len(points)-1]
if lastPoint.Timestamp <= lastAddedTimestamp {
result[len(result)-1] = lastPoint
return result
}
result = append(result, lastPoint)
return result
}
@@ -1103,18 +1106,34 @@ func convertMetricsForChart(
if !sparklineMetrics[metricType] {
continue
}
points := make([]MetricPoint, len(metricPoints))
for i, point := range metricPoints {
ts := point.Timestamp.UnixMilli()
if ts < *oldestTimestamp {
if len(metricPoints) <= maxPoints || maxPoints <= 0 {
points := make([]MetricPoint, len(metricPoints))
for i, point := range metricPoints {
ts := point.Timestamp.UnixMilli()
if ts < *oldestTimestamp {
*oldestTimestamp = ts
}
points[i] = MetricPoint{Timestamp: ts, Value: point.Value}
}
converted[metricType] = points
continue
}
// Oldest-data metadata describes the source observations, not just
// those selected for display (also for degenerate/unsorted inputs).
for _, point := range metricPoints {
if ts := point.Timestamp.UnixMilli(); ts < *oldestTimestamp {
*oldestTimestamp = ts
}
points[i] = MetricPoint{
Timestamp: ts,
Value: point.Value,
}
}
converted[metricType] = capMetricPointSeries(points, maxPoints)
indices := chartPointIndices(len(metricPoints), maxPoints, func(i int) int64 {
return metricPoints[i].Timestamp.UnixMilli()
})
points := make([]MetricPoint, len(indices))
for i, index := range indices {
point := metricPoints[index]
points[i] = MetricPoint{Timestamp: point.Timestamp.UnixMilli(), Value: point.Value}
}
converted[metricType] = points
}
return converted
}
+26
View File
@@ -27781,3 +27781,29 @@ func TestContractSSOAmbiguousSavePreservesDetail(t *testing.T) {
t.Fatal("detail lost its access selection or exposed the saved secret")
}
}
// The cap's degenerate-time fallback selects actual observations. Equal
// timestamps cannot authorise averaging values or replacing explicit zero.
func TestContract_ChartIndexFallbackPreservesObservations(t *testing.T) {
points := []chartapi.MetricPoint{
{Timestamp: 1800000000123, Value: 0.125},
{Timestamp: 1800000000123, Value: 91.75},
{Timestamp: 1800000000123, Value: -7.5},
{Timestamp: 1800000000123, Value: 63.875},
{Timestamp: 1800000000123, Value: 0},
}
for _, tc := range []struct {
cap int
indices []int
}{{0, []int{0, 1, 2, 3, 4}}, {1, []int{4}}, {3, []int{0, 2, 4}}, {4, []int{0, 1, 3, 4}}} {
got := chartapi.CapMetricPointSeriesByIndex(points, tc.cap)
if len(got) != len(tc.indices) {
t.Fatalf("cap %d: %d observations, want %d", tc.cap, len(got), len(tc.indices))
}
for i, index := range tc.indices {
if got[i] != points[index] {
t.Fatalf("cap %d index %d: observation changed: %+v", tc.cap, index, got[i])
}
}
}
}