Pārlūkot izejas kodu

Add underrun streak telemetry

tags/v0.9.0
Jan Svabenik pirms 1 mēnesi
vecāks
revīzija
8d43cf6bad
4 mainītis faili ar 64 papildinājumiem un 0 dzēšanām
  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 Parādīt failu

@@ -95,6 +95,8 @@ Live engine and driver telemetry. Only populated when TX is active.
"framesWritten": 12345,
"samplesWritten": 1408950000,
"underruns": 0,
"underrunStreak": 0,
"maxUnderrunStreak": 0,
"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.

`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`


+ 2
- 0
docs/pro-runtime-hardening-workboard.md Parādīt failu

@@ -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 | 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
| 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 | 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 Parādīt failu

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

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

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

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

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 wraps a StreamSource and rate-converts from the stream's


+ 30
- 0
internal/audio/stream_test.go Parādīt failu

@@ -45,6 +45,36 @@ func TestStreamSource_Underrun(t *testing.T) {
if s.Underruns.Load() != 1 {
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) {


Notiek ielāde…
Atcelt
Saglabāt