Просмотр исходного кода

ws02: expose runtime state age

tags/v0.9.0
Jan Svabenik 1 месяц назад
Родитель
Сommit
21a38d8ab2
4 измененных файлов: 104 добавлений и 71 удалений
  1. +22
    -21
      cmd/fmrtx/main.go
  2. +3
    -0
      docs/API.md
  3. +66
    -49
      internal/app/engine.go
  4. +13
    -1
      internal/control/ui.html

+ 22
- 21
cmd/fmrtx/main.go Просмотреть файл

@@ -252,27 +252,28 @@ func (b *txBridge) StopTX() error { return b.engine.Stop(context.Background())
func (b *txBridge) TXStats() map[string]any {
s := b.engine.Stats()
return map[string]any{
"state": s.State,
"chunksProduced": s.ChunksProduced,
"totalSamples": s.TotalSamples,
"underruns": s.Underruns,
"lateBuffers": s.LateBuffers,
"lastError": s.LastError,
"uptimeSeconds": s.UptimeSeconds,
"maxCycleMs": s.MaxCycleMs,
"maxGenerateMs": s.MaxGenerateMs,
"maxUpsampleMs": s.MaxUpsampleMs,
"maxWriteMs": s.MaxWriteMs,
"queue": s.Queue,
"runtimeIndicator": s.RuntimeIndicator,
"runtimeAlert": s.RuntimeAlert,
"degradedTransitions": s.DegradedTransitions,
"mutedTransitions": s.MutedTransitions,
"faultedTransitions": s.FaultedTransitions,
"faultCount": s.FaultCount,
"faultHistory": s.FaultHistory,
"transitionHistory": s.TransitionHistory,
"lastFault": s.LastFault,
"runtimeStateDurationSeconds": s.RuntimeStateDurationSeconds,
"state": s.State,
"chunksProduced": s.ChunksProduced,
"totalSamples": s.TotalSamples,
"underruns": s.Underruns,
"lateBuffers": s.LateBuffers,
"lastError": s.LastError,
"uptimeSeconds": s.UptimeSeconds,
"maxCycleMs": s.MaxCycleMs,
"maxGenerateMs": s.MaxGenerateMs,
"maxUpsampleMs": s.MaxUpsampleMs,
"maxWriteMs": s.MaxWriteMs,
"queue": s.Queue,
"runtimeIndicator": s.RuntimeIndicator,
"runtimeAlert": s.RuntimeAlert,
"degradedTransitions": s.DegradedTransitions,
"mutedTransitions": s.MutedTransitions,
"faultedTransitions": s.FaultedTransitions,
"faultCount": s.FaultCount,
"faultHistory": s.FaultHistory,
"transitionHistory": s.TransitionHistory,
"lastFault": s.LastFault,
}
}
func (b *txBridge) UpdateConfig(lp ctrlpkg.LivePatch) error {


+ 3
- 0
docs/API.md Просмотреть файл

@@ -59,6 +59,7 @@ Live engine and driver telemetry. Only populated when TX is active.
{
"engine": {
"state": "running",
"runtimeStateDurationSeconds": 12.4,
"chunksProduced": 12345,
"totalSamples": 1408950000,
"underruns": 0,
@@ -100,6 +101,8 @@ Live engine and driver telemetry. Only populated when TX is active.
```
`engine.state` spiegelt jetzt die Runtime-State-Maschine wider (idle, arming, prebuffering, running, degraded, muted, faulted, stopping) und bietet eine erste beobachtbare Basis für Fault-Transitions.

`runtimeStateDurationSeconds` sagt, wie viele Sekunden die Engine bereits im aktuellen Runtime-Zustand verweilt. So erkennt man schnell, ob `muted`/`degraded` zu lange dauern oder ob ein Übergang gerade frisch begonnen hat.

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

---


+ 66
- 49
internal/app/engine.go Просмотреть файл

@@ -69,27 +69,28 @@ func durationMs(ns uint64) float64 {
}

type EngineStats struct {
State string `json:"state"`
ChunksProduced uint64 `json:"chunksProduced"`
TotalSamples uint64 `json:"totalSamples"`
Underruns uint64 `json:"underruns"`
LateBuffers uint64 `json:"lateBuffers,omitempty"`
LastError string `json:"lastError,omitempty"`
UptimeSeconds float64 `json:"uptimeSeconds"`
MaxCycleMs float64 `json:"maxCycleMs,omitempty"`
MaxGenerateMs float64 `json:"maxGenerateMs,omitempty"`
MaxUpsampleMs float64 `json:"maxUpsampleMs,omitempty"`
MaxWriteMs float64 `json:"maxWriteMs,omitempty"`
Queue output.QueueStats `json:"queue"`
RuntimeIndicator RuntimeIndicator `json:"runtimeIndicator"`
RuntimeAlert string `json:"runtimeAlert,omitempty"`
LastFault *FaultEvent `json:"lastFault,omitempty"`
DegradedTransitions uint64 `json:"degradedTransitions"`
MutedTransitions uint64 `json:"mutedTransitions"`
FaultedTransitions uint64 `json:"faultedTransitions"`
FaultCount uint64 `json:"faultCount"`
FaultHistory []FaultEvent `json:"faultHistory,omitempty"`
TransitionHistory []RuntimeTransition `json:"transitionHistory,omitempty"`
State string `json:"state"`
RuntimeStateDurationSeconds float64 `json:"runtimeStateDurationSeconds"`
ChunksProduced uint64 `json:"chunksProduced"`
TotalSamples uint64 `json:"totalSamples"`
Underruns uint64 `json:"underruns"`
LateBuffers uint64 `json:"lateBuffers,omitempty"`
LastError string `json:"lastError,omitempty"`
UptimeSeconds float64 `json:"uptimeSeconds"`
MaxCycleMs float64 `json:"maxCycleMs,omitempty"`
MaxGenerateMs float64 `json:"maxGenerateMs,omitempty"`
MaxUpsampleMs float64 `json:"maxUpsampleMs,omitempty"`
MaxWriteMs float64 `json:"maxWriteMs,omitempty"`
Queue output.QueueStats `json:"queue"`
RuntimeIndicator RuntimeIndicator `json:"runtimeIndicator"`
RuntimeAlert string `json:"runtimeAlert,omitempty"`
LastFault *FaultEvent `json:"lastFault,omitempty"`
DegradedTransitions uint64 `json:"degradedTransitions"`
MutedTransitions uint64 `json:"mutedTransitions"`
FaultedTransitions uint64 `json:"faultedTransitions"`
FaultCount uint64 `json:"faultCount"`
FaultHistory []FaultEvent `json:"faultHistory,omitempty"`
TransitionHistory []RuntimeTransition `json:"transitionHistory,omitempty"`
}

type RuntimeIndicator string
@@ -158,10 +159,11 @@ type Engine struct {
transitionHistoryMu sync.Mutex
transitionHistory []RuntimeTransition

degradedTransitions atomic.Uint64
mutedTransitions atomic.Uint64
faultedTransitions atomic.Uint64
faultEvents atomic.Uint64
degradedTransitions atomic.Uint64
mutedTransitions atomic.Uint64
faultedTransitions atomic.Uint64
faultEvents atomic.Uint64
runtimeStateEnteredAt atomic.Uint64

// Live config: pending frequency change, applied between chunks
pendingFreq atomic.Pointer[float64]
@@ -415,27 +417,28 @@ func (e *Engine) Stats() EngineStats {
ri := runtimeIndicator(queue.Health, hasRecentLateBuffers)
lastFault := e.lastFaultEvent()
return EngineStats{
State: string(e.currentRuntimeState()),
ChunksProduced: e.chunksProduced.Load(),
TotalSamples: e.totalSamples.Load(),
Underruns: e.underruns.Load(),
LateBuffers: lateBuffers,
LastError: errVal,
UptimeSeconds: uptime,
MaxCycleMs: durationMs(e.maxCycleNs.Load()),
MaxGenerateMs: durationMs(e.maxGenerateNs.Load()),
MaxUpsampleMs: durationMs(e.maxUpsampleNs.Load()),
MaxWriteMs: durationMs(e.maxWriteNs.Load()),
Queue: queue,
RuntimeIndicator: ri,
RuntimeAlert: runtimeAlert(queue.Health, hasRecentLateBuffers),
LastFault: lastFault,
DegradedTransitions: e.degradedTransitions.Load(),
MutedTransitions: e.mutedTransitions.Load(),
FaultedTransitions: e.faultedTransitions.Load(),
FaultCount: e.faultEvents.Load(),
FaultHistory: e.FaultHistory(),
TransitionHistory: e.TransitionHistory(),
State: string(e.currentRuntimeState()),
RuntimeStateDurationSeconds: e.runtimeStateDurationSeconds(),
ChunksProduced: e.chunksProduced.Load(),
TotalSamples: e.totalSamples.Load(),
Underruns: e.underruns.Load(),
LateBuffers: lateBuffers,
LastError: errVal,
UptimeSeconds: uptime,
MaxCycleMs: durationMs(e.maxCycleNs.Load()),
MaxGenerateMs: durationMs(e.maxGenerateNs.Load()),
MaxUpsampleMs: durationMs(e.maxUpsampleNs.Load()),
MaxWriteMs: durationMs(e.maxWriteNs.Load()),
Queue: queue,
RuntimeIndicator: ri,
RuntimeAlert: runtimeAlert(queue.Health, hasRecentLateBuffers),
LastFault: lastFault,
DegradedTransitions: e.degradedTransitions.Load(),
MutedTransitions: e.mutedTransitions.Load(),
FaultedTransitions: e.faultedTransitions.Load(),
FaultCount: e.faultEvents.Load(),
FaultHistory: e.FaultHistory(),
TransitionHistory: e.TransitionHistory(),
}
}

@@ -613,9 +616,10 @@ func cloneFrame(src *output.CompositeFrame) *output.CompositeFrame {
}

func (e *Engine) setRuntimeState(state RuntimeState) {
now := time.Now()
prev := e.currentRuntimeState()
if prev != state {
e.recordRuntimeTransition(prev, state)
e.recordRuntimeTransition(prev, state, now)
switch state {
case RuntimeStateDegraded:
e.degradedTransitions.Add(1)
@@ -624,6 +628,9 @@ func (e *Engine) setRuntimeState(state RuntimeState) {
case RuntimeStateFaulted:
e.faultedTransitions.Add(1)
}
e.runtimeStateEnteredAt.Store(uint64(now.UnixNano()))
} else if e.runtimeStateEnteredAt.Load() == 0 {
e.runtimeStateEnteredAt.Store(uint64(now.UnixNano()))
}
e.runtimeState.Store(state)
}
@@ -637,6 +644,13 @@ func (e *Engine) currentRuntimeState() RuntimeState {
return RuntimeStateIdle
}

func (e *Engine) runtimeStateDurationSeconds() float64 {
if ts := e.runtimeStateEnteredAt.Load(); ts != 0 {
return time.Since(time.Unix(0, int64(ts))).Seconds()
}
return 0
}

func (e *Engine) hasRecentLateBuffers() bool {
lateAlertAt := e.lateBufferAlertAt.Load()
if lateAlertAt == 0 {
@@ -670,9 +684,12 @@ func (e *Engine) TransitionHistory() []RuntimeTransition {
return history
}

func (e *Engine) recordRuntimeTransition(from, to RuntimeState) {
func (e *Engine) recordRuntimeTransition(from, to RuntimeState, when time.Time) {
if when.IsZero() {
when = time.Now()
}
ev := RuntimeTransition{
Time: time.Now(),
Time: when,
From: from,
To: to,
Severity: runtimeStateSeverity(to),


+ 13
- 1
internal/control/ui.html Просмотреть файл

@@ -1160,6 +1160,7 @@ input.input-error {
<div class="sidebar-title">Health</div>
<div class="health-line"><div class="name">HTTP</div><div class="val" id="health-http">--</div></div>
<div class="health-line"><div class="name">Runtime</div><div class="val" id="health-runtime">--</div></div>
<div class="health-line"><div class="name">State Age</div><div class="val" id="health-state-age">--</div></div>
<div class="health-line"><div class="name">Runtime Signal</div><div class="val" id="health-indicator">--</div></div>
<div class="health-line"><div class="name">Runtime Alert</div><div class="val" id="health-alert">--</div></div>
<div class="health-line"><div class="name">Transitions (D/M/F)</div><div class="val" id="health-transitions">--</div></div>
@@ -2032,6 +2033,14 @@ function updateHealth(engine, audioStream) {
updateText('health-runtime', runtimeLabel);
$('health-runtime').className = 'val ' + runtimeClass;

const durationSeconds = Number(engine.runtimeStateDurationSeconds);
const durationLabel = Number.isFinite(durationSeconds) && durationSeconds > 0 ? fmtTime(durationSeconds) : '--';
updateText('health-state-age', durationLabel);
const stateAgeEl = $('health-state-age');
if (stateAgeEl) {
stateAgeEl.className = 'val ' + runtimeClass;
}

const runtimeIndicator = engine.runtimeIndicator;
const indicatorLabels = {
normal: 'Normal',
@@ -2142,7 +2151,10 @@ function updateResetHint(engine) {
} else if (stateName === 'muted' || stateName === 'degraded') {
text = 'Reset Fault keeps the runtime in DEGRADED so the queue can recover before running again.';
}
hint.textContent = text;
const durationSeconds = Number(engine?.runtimeStateDurationSeconds);
const durationLabel = Number.isFinite(durationSeconds) && durationSeconds > 0 ? fmtTime(durationSeconds) : null;
const ageHint = durationLabel ? ` State age ${durationLabel}.` : '';
hint.textContent = text + ageHint;
}

function updateMeters(engine, driver, audioStream) {


Загрузка…
Отмена
Сохранить