fixed and improved ui

This commit is contained in:
Martino Ferrari
2026-08-29 23:17:41 +02:00
parent 044ce57ba3
commit ec0a0cdb12
17 changed files with 997 additions and 198 deletions
+52 -9
View File
@@ -92,6 +92,14 @@ type triggerEngine struct {
bufGrowth float64
bufKnown bool
bufRateOK bool
// bufCoverage is the maximum span (seconds) the ring can reach at its
// current bucket and capacity — the gate must never demand more than this,
// or a ring whose coverage is below the window can never satisfy it. 0 =
// unknown (no measurable rate).
bufCoverage float64
// bufArchived is true when the disk history already spans the trigger
// window, so a short capture's front can be back-filled from it.
bufArchived bool
// Reference point the growth is measured against.
bufRefSpan, bufRefWall float64
@@ -179,6 +187,7 @@ func (te *triggerEngine) SetConfig(cfg trigConfig) {
if base != te.baseKey {
// The buffer measurement belongs to the old signal's ring.
te.bufKnown, te.bufRateOK = false, false
te.bufCoverage, te.bufArchived = 0, false
}
te.baseKey, te.elemIdx = base, idx
te.prevValid = false
@@ -285,13 +294,16 @@ const bufGrowthIntervalSec = 0.5
const bufGrowthSmooth = 0.5
// setBuffered records how far back the trigger signal's ring reaches, at wall
// clock now, and derives how fast that is growing. Pass known=false when there
// is no such ring.
func (te *triggerEngine) setBuffered(span float64, known bool, now float64) {
// clock now, and derives how fast that is growing. coverage is the maximum
// span (seconds) the ring can reach at its current bucket/capacity; archived
// says the disk history already spans the trigger window. Pass known=false when
// there is no ring to measure.
func (te *triggerEngine) setBuffered(span, coverage float64, archived, known bool, now float64) {
te.mu.Lock()
defer te.mu.Unlock()
if !known {
te.bufKnown, te.bufRateOK = false, false
te.bufCoverage, te.bufArchived = 0, false
return
}
if !te.bufKnown {
@@ -299,6 +311,8 @@ func (te *triggerEngine) setBuffered(span float64, known bool, now float64) {
te.bufRefSpan, te.bufRefWall = span, now
}
te.bufSpan = span
te.bufCoverage = coverage
te.bufArchived = archived
dt := now - te.bufRefWall
if dt < bufGrowthIntervalSec {
return
@@ -336,9 +350,17 @@ func (te *triggerEngine) setBuffered(span float64, known bool, now float64) {
// anyway. A full one grows only as fast as its incoming samples free space —
// re-bucketing to a longer window replaces dense old samples with sparse new
// ones — and it is that case, growth well below 1, where firing on the
// pre-window alone delivers a capture whose front has been overwritten by the
// time it is read. In the steady state growth is 0 and need is the whole
// window, which a ring tuned for that window already exceeds, so nothing waits.
//
// Two escapes keep an armed trigger from staying deaf forever:
//
// - archived — the disk history already spans the window, so the front of a
// capture can be back-filled from it; the ring only needs to
// hold the pre-window worth of recent data.
// - coverage — never demand more than the ring can physically reach. If its
// coverage saturates below the window (a measured source rate
// that over-estimates the true one), the gate opens once the
// ring is full anyway and a short capture is delivered instead
// of deafness.
func (te *triggerEngine) fillNeedLocked() float64 {
pre := te.cfg.windowSec * te.cfg.prePercent / 100
growth := 0.0 // until measured, assume the buffer will not fill on its own
@@ -349,6 +371,14 @@ func (te *triggerEngine) fillNeedLocked() float64 {
if need < pre {
need = pre
}
if te.bufArchived {
// The archive back-fills the front; the ring holds the post-trigger
// window live, so the pre-window is all it needs to have reached.
return pre
}
if te.bufCoverage > 0 && need > te.bufCoverage {
need = te.bufCoverage
}
return need
}
@@ -655,19 +685,32 @@ func (h *Hub) refreshTriggerFill() {
return
}
now := float64(time.Now().UnixNano()) / 1e9
key := h.trigger.baseSignalKey()
var rb *sigRing
if key := h.trigger.baseSignalKey(); key != "" {
if key != "" {
rb = h.getRing(key)
}
if rb == nil {
// Nothing to measure. Do not gate on a signal the hub does not carry:
// that would leave the trigger armed forever, which is worse than a
// short capture.
h.trigger.setBuffered(0, false, now)
h.trigger.setBuffered(0, 0, false, false, now)
return
}
_, span := rb.stats()
h.trigger.setBuffered(span, true, now)
// Maximum span the ring can ever reach at its current bucket/capacity, in
// seconds. The gate must never demand more than this, or a ring whose
// coverage is below the window (a measured source rate that over-estimates
// the true one) can never satisfy it.
coverage := 0.0
if rate := rb.sourceRate(); rate > 0 {
coverage = float64(ringCoverage(rb.bucketSize(), rb.capacity())) / rate
}
// If the disk archive already spans the trigger window, the front of a
// short capture can be back-filled from it, so the ring need not cover the
// whole window on its own.
archived := h.hist.coversWindow(key, h.trigger.Config().windowSec)
h.trigger.setBuffered(span, coverage, archived, true, now)
}
// triggerTick services the trigger FSM; called from Hub.Run() on every push tick.