84 lines
2.8 KiB
Go
84 lines
2.8 KiB
Go
package wshub
|
|
|
|
import (
|
|
"sort"
|
|
"sync"
|
|
)
|
|
|
|
// capturedWindow is one delivered trigger capture, held at the resolution the
|
|
// rings had when it was taken. Nothing mutates it after publication, so readers
|
|
// may sub-slice it without copying.
|
|
type capturedWindow struct {
|
|
t0, t1 float64
|
|
sigs map[string]sigData
|
|
}
|
|
|
|
// captureHold is the read half of the trigger double buffer; the rings are the
|
|
// write half.
|
|
//
|
|
// The rings keep rolling while the trigger re-arms and collects the next shot,
|
|
// so within seconds of a capture they no longer hold the window the user is
|
|
// looking at — a zoom into it came back with only the newest sliver, or with
|
|
// nothing. Publishing the window here at capture time gives the viewer a
|
|
// snapshot that the re-arming acquisition cannot overwrite: the swap happens
|
|
// only when the *next* capture is complete, which is also the moment the client
|
|
// stops displaying this one.
|
|
type captureHold struct {
|
|
mu sync.RWMutex
|
|
cur *capturedWindow
|
|
}
|
|
|
|
// publish swaps in a new capture, retiring the previous one. Readers that
|
|
// already hold a pointer to the retired window keep reading it safely.
|
|
func (ch *captureHold) publish(t0, t1 float64, sigs map[string]sigData) {
|
|
if len(sigs) == 0 {
|
|
return
|
|
}
|
|
w := &capturedWindow{t0: t0, t1: t1, sigs: sigs}
|
|
ch.mu.Lock()
|
|
ch.cur = w
|
|
ch.mu.Unlock()
|
|
}
|
|
|
|
// clear drops the held capture, releasing its memory.
|
|
func (ch *captureHold) clear() {
|
|
ch.mu.Lock()
|
|
ch.cur = nil
|
|
ch.mu.Unlock()
|
|
}
|
|
|
|
// slice answers [a, b] for one signal out of the held capture, reporting
|
|
// whether it could.
|
|
//
|
|
// It declines any range reaching outside the captured window: that is a live
|
|
// zoom or a pan off the capture, and only the rings still track the stream.
|
|
// Inside the window the hold is never worse than the rings — retuning does not
|
|
// rewrite stored samples, so a ring that still covers the range holds the very
|
|
// same points — which is why no trigger-state gating is needed here.
|
|
func (ch *captureHold) slice(key string, a, b float64) ([]float64, []float64, bool) {
|
|
ch.mu.RLock()
|
|
w := ch.cur
|
|
ch.mu.RUnlock()
|
|
if w == nil || a < w.t0 || b > w.t1 {
|
|
return nil, nil, false
|
|
}
|
|
sd, ok := w.sigs[key]
|
|
if !ok || len(sd.T) == 0 {
|
|
return nil, nil, false
|
|
}
|
|
// The window is what was asked for; this signal's samples are what could be
|
|
// found. A capture whose front was never recoverable must not answer for the
|
|
// stretch it is missing — the client would redraw the same hole on every
|
|
// zoom and every "fit" instead of falling back to the archive.
|
|
tol := shortCaptureTol * (w.t1 - w.t0)
|
|
if sd.T[0] > a+tol || sd.T[len(sd.T)-1] < b-tol {
|
|
return nil, nil, false
|
|
}
|
|
lo := sort.SearchFloat64s(sd.T, a)
|
|
hi := lo + sort.Search(len(sd.T)-lo, func(i int) bool { return sd.T[lo+i] > b })
|
|
if hi <= lo {
|
|
return nil, nil, false
|
|
}
|
|
return sd.T[lo:hi], sd.V[lo:hi], true
|
|
}
|