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 }