Преглед изворни кода

Add underrun streak telemetry

tags/v0.9.0
Jan Svabenik пре 1 месец
родитељ
комит
8d43cf6bad
4 измењених фајлова са 64 додато и 0 уклоњено
  1. +5
    -0
      docs/API.md
  2. +2
    -0
      docs/pro-runtime-hardening-workboard.md
  3. +27
    -0
      internal/audio/stream.go
  4. +30
    -0
      internal/audio/stream_test.go

+ 5
- 0
docs/API.md Прегледај датотеку

@@ -95,6 +95,8 @@ Live engine and driver telemetry. Only populated when TX is active.
"framesWritten": 12345, "framesWritten": 12345,
"samplesWritten": 1408950000, "samplesWritten": 1408950000,
"underruns": 0, "underruns": 0,
"underrunStreak": 0,
"maxUnderrunStreak": 0,
"effectiveSampleRateHz": 2280000 "effectiveSampleRateHz": 2280000
} }
} }
@@ -105,6 +107,9 @@ Live engine and driver telemetry. Only populated when TX is active.


`transitionHistory` liefert die jüngsten Übergänge (from/to, severity, timestamp) damit API und UI die Runtime History synchronisieren können. `transitionHistory` liefert die jüngsten Übergänge (from/to, severity, timestamp) damit API und UI die Runtime History synchronisieren können.


`driver.underrunStreak` reports how many consecutive reads returned silence, and `driver.maxUnderrunStreak` captures the longest such run since the engine started. Together they help differentiate short glitches from persistent underrun storms and can be plotted alongside queue health sparkline telemetry.


--- ---


### `POST /runtime/fault/reset` ### `POST /runtime/fault/reset`


+ 2
- 0
docs/pro-runtime-hardening-workboard.md Прегледај датотеку

@@ -386,12 +386,14 @@ Vollständige Sichtbarkeit auf Runtime, Queue, Writer, Generator, RF-Selbsttests
| --- | --- | --- | | --- | --- | --- |
| 2026-04-06 | High-watermark trend sparkline | Captured audio high-watermark duration history and surface it as a new Health-panel sparkline for queue pressure visibility. | | 2026-04-06 | High-watermark trend sparkline | Captured audio high-watermark duration history and surface it as a new Health-panel sparkline for queue pressure visibility. |
| 2026-04-06 | Queue fill visibility | Added queue fill ratio health line and sparklines to highlight real-time queue pressure alongside high-watermark trends. | | 2026-04-06 | Queue fill visibility | Added queue fill ratio health line and sparklines to highlight real-time queue pressure alongside high-watermark trends. |
| 2026-04-07 | Underrun streak telemetry | StreamStats now expose current and max underrun streak counters so queue diagnostics can see repeated underruns without touching the metrics stack. |


## WS-04 Verifikation ## WS-04 Verifikation
| Datum | Fokus | Ergebnis | | Datum | Fokus | Ergebnis |
| --- | --- | --- | | --- | --- | --- |
| 2026-04-06 | High-watermark trend sparkline | `go test ./...` plus manual UI check confirm the new sparkline updates with runtime audio stats. | | 2026-04-06 | High-watermark trend sparkline | `go test ./...` plus manual UI check confirm the new sparkline updates with runtime audio stats. |
| 2026-04-06 | Queue fill visibility | `go test ./...` plus UI smoke check confirm queue fill stats stay available and the new sparkline/health line react to queue health changes. | | 2026-04-06 | Queue fill visibility | `go test ./...` plus UI smoke check confirm queue fill stats stay available and the new sparkline/health line react to queue health changes. |
| 2026-04-07 | Underrun streak telemetry | `go test ./internal/audio` confirms the new streak counters plus Stats coverage so the API surfaces the same names. |


--- ---




+ 27
- 0
internal/audio/stream.go Прегледај датотеку

@@ -25,6 +25,8 @@ type StreamSource struct {
Overflows atomic.Uint64 Overflows atomic.Uint64
Written atomic.Uint64 Written atomic.Uint64
highWatermark atomic.Int64 highWatermark atomic.Int64
underrunStreak atomic.Uint64
maxUnderrunStreak atomic.Uint64
} }


// NewStreamSource creates a ring buffer with the given capacity (rounded up // NewStreamSource creates a ring buffer with the given capacity (rounded up
@@ -87,10 +89,12 @@ func (s *StreamSource) ReadFrame() Frame {
wp := s.writePos.Load() wp := s.writePos.Load()
if rp >= wp { if rp >= wp {
s.Underruns.Add(1) s.Underruns.Add(1)
s.recordUnderrunStreak()
return NewFrame(0, 0) return NewFrame(0, 0)
} }
f := s.ring[int(rp)&s.mask] f := s.ring[int(rp)&s.mask]
s.readPos.Add(1) s.readPos.Add(1)
s.resetUnderrunStreak()
return f return f
} }


@@ -117,6 +121,8 @@ func (s *StreamSource) Stats() StreamStats {
buffered = float64(available) / float64(s.size) buffered = float64(available) / float64(s.size)
} }
highWatermark := int(s.highWatermark.Load()) highWatermark := int(s.highWatermark.Load())
currentStreak := int(s.underrunStreak.Load())
maxStreak := int(s.maxUnderrunStreak.Load())
return StreamStats{ return StreamStats{
Available: available, Available: available,
Capacity: s.size, Capacity: s.size,
@@ -127,6 +133,8 @@ func (s *StreamSource) Stats() StreamStats {
Written: s.Written.Load(), Written: s.Written.Load(),
Underruns: s.Underruns.Load(), Underruns: s.Underruns.Load(),
Overflows: s.Overflows.Load(), Overflows: s.Overflows.Load(),
UnderrunStreak: currentStreak,
MaxUnderrunStreak: maxStreak,
} }
} }


@@ -141,6 +149,8 @@ type StreamStats struct {
Written uint64 `json:"written"` Written uint64 `json:"written"`
Underruns uint64 `json:"underruns"` Underruns uint64 `json:"underruns"`
Overflows uint64 `json:"overflows"` Overflows uint64 `json:"overflows"`
UnderrunStreak int `json:"underrunStreak"`
MaxUnderrunStreak int `json:"maxUnderrunStreak"`
} }


func (s *StreamSource) bufferedDurationSeconds(available int) float64 { func (s *StreamSource) bufferedDurationSeconds(available int) float64 {
@@ -163,6 +173,23 @@ func (s *StreamSource) updateHighWatermark() {
} }
} }


func (s *StreamSource) recordUnderrunStreak() {
current := s.underrunStreak.Add(1)
for {
prevMax := s.maxUnderrunStreak.Load()
if current <= prevMax {
return
}
if s.maxUnderrunStreak.CompareAndSwap(prevMax, current) {
return
}
}
}

func (s *StreamSource) resetUnderrunStreak() {
s.underrunStreak.Store(0)
}

// --- StreamResampler --- // --- StreamResampler ---


// StreamResampler wraps a StreamSource and rate-converts from the stream's // StreamResampler wraps a StreamSource and rate-converts from the stream's


+ 30
- 0
internal/audio/stream_test.go Прегледај датотеку

@@ -45,6 +45,36 @@ func TestStreamSource_Underrun(t *testing.T) {
if s.Underruns.Load() != 1 { if s.Underruns.Load() != 1 {
t.Fatalf("expected 1 underrun, got %d", s.Underruns.Load()) t.Fatalf("expected 1 underrun, got %d", s.Underruns.Load())
} }
stats := s.Stats()
if stats.UnderrunStreak != 1 || stats.MaxUnderrunStreak != 1 {
t.Fatalf("unexpected streak: %d/%d", stats.UnderrunStreak, stats.MaxUnderrunStreak)
}
}

func TestStreamSource_UnderrunStreakTracking(t *testing.T) {
s := NewStreamSource(16, 44100)
for i := 0; i < 3; i++ {
s.ReadFrame()
}
stats := s.Stats()
if stats.UnderrunStreak != 3 {
t.Fatalf("expected streak 3, got %d", stats.UnderrunStreak)
}
if stats.MaxUnderrunStreak != 3 {
t.Fatalf("expected max streak 3, got %d", stats.MaxUnderrunStreak)
}

if !s.WriteFrame(NewFrame(0, 0)) {
t.Fatal("expected write to succeed")
}
s.ReadFrame()
stats = s.Stats()
if stats.UnderrunStreak != 0 {
t.Fatalf("expected streak reset to 0, got %d", stats.UnderrunStreak)
}
if stats.MaxUnderrunStreak != 3 {
t.Fatalf("expected max streak to stay 3, got %d", stats.MaxUnderrunStreak)
}
} }


func TestStreamSource_Overflow(t *testing.T) { func TestStreamSource_Overflow(t *testing.T) {


Loading…
Откажи
Сачувај