Implemented and fixed many issues

This commit is contained in:
Martino Ferrari
2026-08-21 23:24:48 +02:00
parent 14d5351a81
commit e03c60db25
52 changed files with 7726 additions and 769 deletions
+283 -112
View File
@@ -65,6 +65,8 @@ StreamHub::StreamHub()
statsRateHz_(1u),
ringTemporal_(1000000u),
ringScalar_(100000u),
ringMaxPts_(8388608u),
trigRetentionSec_(0.0),
nextSourceId_(1u),
calibration_(static_cast<CalibrationEntry *>(0)),
numCalibration_(0u),
@@ -79,13 +81,19 @@ StreamHub::StreamHub()
pushV_(static_cast<float64 *>(0)),
lastTrigState_(kTrigIdle),
rearmPending_(false),
rearmAtWallS_(0.0) {
rearmAtWallS_(0.0),
collectStartWallS_(0.0),
capBuf_(static_cast<uint8 *>(0)),
capCap_(0u),
capOff_(0u),
capNSig_(0u) {
memset(&recorderCfg_, 0, sizeof(recorderCfg_));
calibration_ = new CalibrationEntry[kMaxCalibration];
memset(calibration_, 0, sizeof(CalibrationEntry) * kMaxCalibration);
for (uint32 i = 0u; i < kMaxSessions; i++) {
sessionActive_[i] = false;
configBroadcast_[i] = false;
capHarvested_[i] = false;
for (uint32 s = 0u; s < UDPSS_MAX_SIGNALS; s++) {
pushCursor_[i][s] = 0u;
}
@@ -104,6 +112,10 @@ StreamHub::~StreamHub() {
delete[] pushBuf_;
pushBuf_ = static_cast<uint8 *>(0);
}
if (capBuf_ != static_cast<uint8 *>(0)) {
delete[] capBuf_;
capBuf_ = static_cast<uint8 *>(0);
}
if (lttbT_ != static_cast<float64 *>(0)) {
delete[] lttbT_;
lttbT_ = static_cast<float64 *>(0);
@@ -138,11 +150,38 @@ bool StreamHub::Initialise(StructuredDataI &cfg) {
if (cfg.Read("StatsRate", tmp)) { statsRateHz_ = (tmp > 0u) ? tmp : 1u; }
if (cfg.Read("RingTemporal", tmp)) { ringTemporal_ = (tmp > 0u) ? tmp : 1000000u; }
if (cfg.Read("RingScalar", tmp)) { ringScalar_ = (tmp > 0u) ? tmp : 100000u; }
/* Per-signal ceiling when a trigger window forces a ring to grow.
* 128 MiB / (2 × float64) = 8388608 points — ~8 s at 1 Msps, ~1.7 s at
* 5 Msps. Raise it if you need longer windows on very fast sources. */
if (cfg.Read("RingMaxMB", tmp) && (tmp > 0u)) {
ringMaxPts_ = tmp * (1048576u / 16u);
}
if (ringMaxPts_ < ringTemporal_) { ringMaxPts_ = ringTemporal_; }
sourcesFile_ = "streamhub_sources.json";
StreamString sf;
if (cfg.Read("SourcesFile", sf)) { sourcesFile_ = sf; }
/* Origins allowed to open the WebSocket, comma-separated
* ("http://localhost:8080,http://box.lan:8080"). Without this only
* same-origin upgrades pass, which rejects every browser that loaded the
* SPA from a separate web server (the usual deployment). */
StreamString origins;
if (cfg.Read("AllowedOrigins", origins)) {
char list[1024];
strncpy(list, origins.Buffer(), sizeof(list) - 1u);
list[sizeof(list) - 1u] = '\0';
char *tok = strtok(list, ", \t");
while (tok != static_cast<char *>(0)) {
if (!wsServer_.AddAllowedOrigin(tok)) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: rejected allowed-origin '%s' (list full or too long).",
tok);
}
tok = strtok(static_cast<char *>(0), ", \t");
}
}
/* Parse +History block (optional).
* StandardParser stores the '+' prefix in the node name, so we try both. */
if (cfg.MoveRelative("+History") || cfg.MoveRelative("History")) {
@@ -194,8 +233,8 @@ bool StreamHub::Initialise(StructuredDataI &cfg) {
/* Allocate scratch buffers */
pushBuf_ = new uint8[kPushBufSize];
lttbT_ = new float64[maxPushPoints_];
lttbV_ = new float64[maxPushPoints_];
lttbT_ = new float64[kPushScratchPts];
lttbV_ = new float64[kPushScratchPts];
pushT_ = new float64[kPushScratchPts];
pushV_ = new float64[kPushScratchPts];
@@ -309,6 +348,7 @@ bool StreamHub::Run() {
if (statsDivisor == 0u) { statsDivisor = 1u; }
if ((tickCount_ % statsDivisor) == 0u) {
PushStats();
GrowRingsForTrigger();
}
/* History: flush headers at the configured interval, then re-broadcast
@@ -337,11 +377,16 @@ bool StreamHub::Run() {
tickCount_++;
/* Sleep for remainder of period */
/* Sleep for remainder of period. Both operands are unsigned, so the
* comparison must be done additively: a tick that overruns the period
* (easy at multi-Msps ingest, and guaranteed on the first tick, which
* drains the whole ring) would otherwise wrap periodUs - elapsedUs to
* ~2^64 and park the push thread for weeks — no data frames, no stats
* and no trigger captures for the rest of the run. */
uint64 t1 = MARTe::HighResolutionTimer::Counter();
uint64 freq = MARTe::HighResolutionTimer::Frequency();
uint64 elapsedUs = ((t1 - t0) * 1000000u) / freq;
if (periodUs - elapsedUs > 1000) {
if ((elapsedUs + 1000u) < periodUs) {
Sleep::MSec(static_cast<uint32>((periodUs - elapsedUs) / 1000u));
}
}
@@ -468,19 +513,35 @@ uint32 StreamHub::SerializeBinaryFrame(uint32 sessionIdx,
pushT_, pushV_, kPushScratchPts);
if (nRaw == 0u) { continue; }
/* LTTB decimation only for temporal (multi-element, sample-timed)
* signals — Go hub policy. Scalars and PACKET-timed arrays are
* pushed verbatim (their per-tick batches are small). */
/* LTTB decimation for the live push, bounded for every signal.
*
* A PACKET-timed array is a snapshot waveform, so its floor is one
* packet's worth of elements: below that LTTB would flatten the very
* waveform the operator is looking at, above it each extra point is
* just backlog from packets that piled up during the tick. Exempting
* those arrays altogether (the old rule, on the assumption that their
* per-tick batches are small) does not survive a fast producer: the
* 5 kHz x 1000-element time array in the demo pushes ~65k points per
* tick, 31 MB/s — 30x the channel it timestamps — and the per-client
* push queue never drains.
*
* Decimating here costs no fidelity downstream: LTTB picks real
* samples (it never interpolates), the rings keep every sample, and
* zoom/history/trigger all re-read the rings at full resolution. */
const uint32 nElems = desc.numRows * ((desc.numCols > 0u) ? desc.numCols : 1u);
const bool temporal = (nElems > 1u) &&
(desc.timeMode != MARTe::UDPS_TIMEMODE_PACKET);
uint32 threshold = maxPushPoints_;
if ((desc.timeMode == MARTe::UDPS_TIMEMODE_PACKET) &&
(nElems > threshold)) {
threshold = nElems;
}
if (threshold > kPushScratchPts) { threshold = kPushScratchPts; }
const float64 *tOut;
const float64 *vOut;
uint32 nOut;
if (temporal && (nRaw > maxPushPoints_)) {
if (nRaw > threshold) {
nOut = LTTBDecimate(pushT_, pushV_, nRaw,
lttbT_, lttbV_, maxPushPoints_);
lttbT_, lttbV_, threshold);
tOut = lttbT_;
vOut = lttbV_;
} else {
@@ -1556,7 +1617,8 @@ void StreamHub::HandleTrigStop(const char *json) {
void StreamHub::HandleSetTrigger(const char *json) {
/* Web client shape:
* {"type":"setTrigger","signal":"src:sig[i]","edge":"rising|falling|both",
* "threshold":F,"windowSec":F,"prePercent":F,"mode":"normal|single"} */
* "threshold":F,"windowSec":F,"prePercent":F,"mode":"normal|single",
* "holdoffSec":F} */
TriggerConfig cfg = trigger_.GetConfig();
char key[160] = "";
@@ -1565,6 +1627,7 @@ void StreamHub::HandleSetTrigger(const char *json) {
float64 thr = cfg.threshold;
float64 winSec = cfg.windowSec;
float64 prePct = cfg.prePercent;
float64 holdoff = cfg.holdoffSec;
if (JsonGetString(json, "signal", key, sizeof(key))) {
cfg.signalKey = key;
@@ -1580,8 +1643,20 @@ void StreamHub::HandleSetTrigger(const char *json) {
if (JsonGetFloat(json, "threshold", thr)) { cfg.threshold = thr; }
if (JsonGetFloat(json, "windowSec", winSec)) { cfg.windowSec = winSec; }
if (JsonGetFloat(json, "prePercent", prePct)) { cfg.prePercent = prePct; }
if (JsonGetFloat(json, "holdoffSec", holdoff)) { cfg.holdoffSec = holdoff; }
trigger_.SetConfig(cfg);
/* A capture can only contain what the rings still hold: the default
* capacity is a point count, so at 1 Msps it covers ~1 s and every longer
* window came back with only its tail populated. Publish the requested
* retention so the push thread can size the rings to the actual measured
* sample rate. */
trigRetentionSec_ = cfg.windowSec;
/* Grow now, not on the next stats tick: clients send setTrigger and arm
* back to back, and a trigger that fires before the rings are resized
* still loses its pre-trigger data. The periodic call stays as the catch-up
* path for sources that connect later. */
GrowRingsForTrigger();
BroadcastTriggerState();
}
@@ -1593,22 +1668,67 @@ void StreamHub::TriggerTick(float64 wallNowS) {
/* Capture-margin: wait a little past the post window so the rings have
* received the last post-trigger samples (web client used 120 ms). */
static const float64 kCaptureMarginS = 0.15;
static const float64 kAutoRearmDelayS = 0.2;
const TrigState st = trigger_.GetState();
/* Wall-clock grace on top of the post window before giving up on a source
* that stopped advancing; the capture is then broadcast with whatever the
* rings hold. */
static const float64 kCaptureWatchdogS = 2.0;
if (st == kTrigCollecting) {
const bool justEntered = (lastTrigState_ != kTrigCollecting);
if (justEntered) { collectStartWallS_ = wallNowS; }
float64 trigTime = 0.0;
float64 preSec = 0.0;
float64 postSec = 0.0;
if (trigger_.GetFiredWindow(trigTime, preSec, postSec) &&
(wallNowS >= (trigTime + postSec + kCaptureMarginS))) {
BroadcastTriggerCapture(trigTime, preSec, postSec);
trigger_.MarkTriggered();
TriggerConfig cfg = trigger_.GetConfig();
if ((cfg.mode == kTrigNormal) && !trigger_.GetStopped()) {
rearmPending_ = true;
rearmAtWallS_ = wallNowS + kAutoRearmDelayS;
if (trigger_.GetFiredWindow(trigTime, preSec, postSec)) {
/* Always restart the frame on entry: a capture abandoned by a
* disarm would otherwise be resumed with the previous trigTime. */
if (justEntered || (capBuf_ == static_cast<MARTe::uint8 *>(0))) {
BeginTriggerCapture(trigTime, preSec, postSec);
}
/* Harvest on the *data's* clock. trigTime comes from the sample
* timestamps, and a source's time base is offset from — and drifts
* against — CLOCK_REALTIME, so a wall-clock deadline chops the tail
* off every capture by exactly that offset. The wall clock is only
* a watchdog for a source that went quiet. */
const bool timedOut =
(wallNowS >= (collectStartWallS_ + postSec + kCaptureWatchdogS));
const float64 deadline = trigTime + postSec + kCaptureMarginS;
bool allDone = true;
for (uint32 i = 0u; i < kMaxSessions; i++) {
if (!sessionActive_[i] || capHarvested_[i]) { continue; }
const float64 frontier = SourceFrontierTime(i, wallNowS);
if (frontier >= deadline) {
HarvestTriggerCapture(i, trigTime - preSec,
trigTime + postSec);
}
else if (timedOut) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Warning,
"StreamHub: source %s timed out at %.3f s of the %.3f s "
"trigger window; its traces will be short.",
sessions_[i].GetId().Buffer(), frontier - trigTime,
postSec);
HarvestTriggerCapture(i, trigTime - preSec,
trigTime + postSec);
}
else {
allDone = false;
}
}
if (allDone || timedOut) {
FinishTriggerCapture();
trigger_.MarkTriggered();
TriggerConfig cfg = trigger_.GetConfig();
if ((cfg.mode == kTrigNormal) && !trigger_.GetStopped()) {
rearmPending_ = true;
rearmAtWallS_ = wallNowS + cfg.holdoffSec;
}
}
}
}
@@ -1627,6 +1747,45 @@ void StreamHub::TriggerTick(float64 wallNowS) {
}
}
float64 StreamHub::SourceFrontierTime(uint32 i, float64 wallNowS) const {
const float64 t = sessions_[i].ProducerNewestTime();
/* No producer clock means every sample was stamped on arrival, so this
* source and the trigger both live in the hub's wall-clock domain. */
return (t > 0.0) ? t : wallNowS;
}
uint32 StreamHub::CurrentMaxRingCapacity() const {
/* Rings grow at runtime for long trigger windows, so read the live
* capacities rather than the configured starting size. */
uint32 maxCap = (ringTemporal_ > ringScalar_) ? ringTemporal_ : ringScalar_;
for (uint32 i = 0u; i < kMaxSessions; i++) {
if (!sessionActive_[i]) { continue; }
const uint32 c = sessions_[i].GetMaxRingCapacity();
if (c > maxCap) { maxCap = c; }
}
return maxCap;
}
void StreamHub::GrowRingsForTrigger() {
const float64 want = trigRetentionSec_;
if (want <= 0.0) { return; }
/* Retain the whole window plus the capture margin and one push period, so
* the tail of the window is still in the ring when TriggerTick reads it. */
const float64 target = want + 0.5;
for (uint32 i = 0u; i < kMaxSessions; i++) {
if (!sessionActive_[i]) { continue; }
if (!sessions_[i].IsConfigured()) { continue; }
if (sessions_[i].GrowRingsForSeconds(target, ringMaxPts_)) {
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"StreamHub: grew rings of source %s to hold %.2f s "
"(trigger window %.2f s, cap %u pts/signal).",
sessions_[i].GetId().Buffer(), target, want, ringMaxPts_);
}
}
}
void StreamHub::BroadcastTriggerState() {
const TrigState st = trigger_.GetState();
TriggerConfig cfg = trigger_.GetConfig();
@@ -1639,17 +1798,23 @@ void StreamHub::BroadcastTriggerState() {
(st == kTrigTriggered) ? "triggered" : "idle";
const char *modeStr = (cfg.mode == kTrigSingle) ? "single" : "normal";
char buf[256];
char buf[512];
int n;
float64 trigTime = 0.0;
float64 preSec = 0.0;
float64 postSec = 0.0;
if (((st == kTrigCollecting) || (st == kTrigTriggered)) &&
trigger_.GetFiredWindow(trigTime, preSec, postSec)) {
/* The window latched at fire time. Clients draw the filling capture on
* this axis before the v2 frame arrives, and config edits between arm
* and fire would otherwise leave them inferring the wrong window from
* their own copy of the config. */
n = snprintf(buf, sizeof(buf),
"{\"type\":\"triggerState\",\"state\":\"%s\",\"mode\":\"%s\","
"\"stopped\":%s,\"trigTime\":%.17g}",
stateStr, modeStr, (stopped ? "true" : "false"), trigTime);
"\"stopped\":%s,\"trigTime\":%.17g,\"preSec\":%.17g,"
"\"postSec\":%.17g}",
stateStr, modeStr, (stopped ? "true" : "false"), trigTime,
preSec, postSec);
} else {
n = snprintf(buf, sizeof(buf),
"{\"type\":\"triggerState\",\"state\":\"%s\",\"mode\":\"%s\","
@@ -1661,109 +1826,116 @@ void StreamHub::BroadcastTriggerState() {
}
}
void StreamHub::BroadcastTriggerCapture(float64 trigTime, float64 preSec,
float64 postSec) {
const float64 t0 = trigTime - preSec;
const float64 t1 = trigTime + postSec;
void StreamHub::BeginTriggerCapture(float64 trigTime, float64 preSec,
float64 postSec) {
delete[] capBuf_;
capCap_ = 1u << 20;
capBuf_ = new uint8[capCap_];
capOff_ = 0u;
capNSig_ = 0u;
for (uint32 i = 0u; i < kMaxSessions; i++) { capHarvested_[i] = false; }
/* Header: [u8 2][f64 trigTime][f64 preSec][f64 postSec][u32 nSig] */
capBuf_[capOff_++] = 2u;
memcpy(capBuf_ + capOff_, &trigTime, 8u); capOff_ += 8u;
memcpy(capBuf_ + capOff_, &preSec, 8u); capOff_ += 8u;
memcpy(capBuf_ + capOff_, &postSec, 8u); capOff_ += 8u;
capOff_ += 4u; /* nSig, patched in FinishTriggerCapture */
}
void StreamHub::HarvestTriggerCapture(uint32 i, float64 t0, float64 t1) {
capHarvested_[i] = true;
UDPSourceSession &sess = sessions_[i];
if ((capBuf_ == static_cast<uint8 *>(0)) || !sess.IsConfigured()) { return; }
/* Read scratch sized for the largest ring; LTTB scratch for the cap. */
const uint32 scratchCap = (ringTemporal_ > ringScalar_) ? ringTemporal_
: ringScalar_;
const uint32 scratchCap = CurrentMaxRingCapacity();
float64 *tRaw = new float64[scratchCap];
float64 *vRaw = new float64[scratchCap];
float64 *tDec = new float64[kTrigCapturePts];
float64 *vDec = new float64[kTrigCapturePts];
uint32 cap = 1u << 20;
uint8 *buf = new uint8[cap];
uint32 off = 0u;
StreamString sid = sess.GetId();
const uint32 numSigs = sess.GetNumSignals();
/* Header: [u8 2][f64 trigTime][f64 preSec][f64 postSec][u32 nSig] */
buf[off++] = 2u;
memcpy(buf + off, &trigTime, 8u); off += 8u;
memcpy(buf + off, &preSec, 8u); off += 8u;
memcpy(buf + off, &postSec, 8u); off += 8u;
const uint32 nSigOff = off;
uint32 nSigWritten = 0u;
off += 4u;
for (uint32 s = 0u; s < numSigs; s++) {
MARTe::UDPSSignalDescriptor desc;
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
for (uint32 i = 0u; i < kMaxSessions; i++) {
if (!sessionActive_[i]) { continue; }
UDPSourceSession &sess = sessions_[i];
if (!sess.IsConfigured()) { continue; }
const uint32 nRaw = sess.ReadSignalRange(s, t0, t1,
tRaw, vRaw, scratchCap);
if (nRaw == 0u) { continue; }
StreamString sid = sess.GetId();
const uint32 numSigs = sess.GetNumSignals();
for (uint32 s = 0u; s < numSigs; s++) {
MARTe::UDPSSignalDescriptor desc;
if (!sess.GetSignalDescriptor(s, desc)) { continue; }
const uint32 nRaw = sess.ReadSignalRange(s, t0, t1,
tRaw, vRaw, scratchCap);
if (nRaw == 0u) { continue; }
const float64 *tOut = tRaw;
const float64 *vOut = vRaw;
uint32 nOut = nRaw;
if (nRaw > kTrigCapturePts) {
nOut = LTTBDecimate(tRaw, vRaw, nRaw, tDec, vDec,
kTrigCapturePts);
tOut = tDec;
vOut = vDec;
}
char fullKey[192];
const int kn = snprintf(fullKey, sizeof(fullKey), "%s:%s",
sid.Buffer(), desc.name);
if (kn <= 0) { continue; }
const uint32 keyLen = static_cast<uint32>(kn);
const uint32 need = 2u + keyLen + 4u + nOut * 16u;
if ((off + need) > cap) {
uint32 newCap = cap * 2u;
while ((off + need) > newCap) { newCap *= 2u; }
uint8 *nb = new uint8[newCap];
memcpy(nb, buf, off);
delete[] buf;
buf = nb;
cap = newCap;
}
buf[off++] = static_cast<uint8>( keyLen & 0xFFu);
buf[off++] = static_cast<uint8>((keyLen >> 8) & 0xFFu);
memcpy(buf + off, fullKey, keyLen);
off += keyLen;
buf[off++] = static_cast<uint8>( nOut & 0xFFu);
buf[off++] = static_cast<uint8>((nOut >> 8) & 0xFFu);
buf[off++] = static_cast<uint8>((nOut >> 16) & 0xFFu);
buf[off++] = static_cast<uint8>((nOut >> 24) & 0xFFu);
memcpy(buf + off, tOut, nOut * sizeof(float64));
off += nOut * 8u;
memcpy(buf + off, vOut, nOut * sizeof(float64));
off += nOut * 8u;
nSigWritten++;
const float64 *tOut = tRaw;
const float64 *vOut = vRaw;
uint32 nOut = nRaw;
if (nRaw > kTrigCapturePts) {
nOut = LTTBDecimate(tRaw, vRaw, nRaw, tDec, vDec, kTrigCapturePts);
tOut = tDec;
vOut = vDec;
}
char fullKey[192];
const int kn = snprintf(fullKey, sizeof(fullKey), "%s:%s",
sid.Buffer(), desc.name);
if (kn <= 0) { continue; }
const uint32 keyLen = static_cast<uint32>(kn);
const uint32 need = 2u + keyLen + 4u + nOut * 16u;
if ((capOff_ + need) > capCap_) {
uint32 newCap = capCap_ * 2u;
while ((capOff_ + need) > newCap) { newCap *= 2u; }
uint8 *nb = new uint8[newCap];
memcpy(nb, capBuf_, capOff_);
delete[] capBuf_;
capBuf_ = nb;
capCap_ = newCap;
}
capBuf_[capOff_++] = static_cast<uint8>( keyLen & 0xFFu);
capBuf_[capOff_++] = static_cast<uint8>((keyLen >> 8) & 0xFFu);
memcpy(capBuf_ + capOff_, fullKey, keyLen);
capOff_ += keyLen;
capBuf_[capOff_++] = static_cast<uint8>( nOut & 0xFFu);
capBuf_[capOff_++] = static_cast<uint8>((nOut >> 8) & 0xFFu);
capBuf_[capOff_++] = static_cast<uint8>((nOut >> 16) & 0xFFu);
capBuf_[capOff_++] = static_cast<uint8>((nOut >> 24) & 0xFFu);
memcpy(capBuf_ + capOff_, tOut, nOut * sizeof(float64));
capOff_ += nOut * 8u;
memcpy(capBuf_ + capOff_, vOut, nOut * sizeof(float64));
capOff_ += nOut * 8u;
capNSig_++;
}
/* Patch nSig */
buf[nSigOff] = static_cast<uint8>( nSigWritten & 0xFFu);
buf[nSigOff + 1u] = static_cast<uint8>((nSigWritten >> 8) & 0xFFu);
buf[nSigOff + 2u] = static_cast<uint8>((nSigWritten >> 16) & 0xFFu);
buf[nSigOff + 3u] = static_cast<uint8>((nSigWritten >> 24) & 0xFFu);
delete[] tRaw; delete[] vRaw;
delete[] tDec; delete[] vDec;
}
wsServer_.BroadcastBinary(buf, off);
void StreamHub::FinishTriggerCapture() {
if (capBuf_ == static_cast<uint8 *>(0)) { return; }
/* Patch nSig (immediately after the [u8 2] + 3×f64 header). */
const uint32 nSigOff = 25u;
capBuf_[nSigOff] = static_cast<uint8>( capNSig_ & 0xFFu);
capBuf_[nSigOff + 1u] = static_cast<uint8>((capNSig_ >> 8) & 0xFFu);
capBuf_[nSigOff + 2u] = static_cast<uint8>((capNSig_ >> 16) & 0xFFu);
capBuf_[nSigOff + 3u] = static_cast<uint8>((capNSig_ >> 24) & 0xFFu);
wsServer_.BroadcastBinary(capBuf_, capOff_);
REPORT_ERROR_STATIC(MARTe::ErrorManagement::Information,
"StreamHub: trigger capture broadcast (%u signal(s), %u bytes).",
nSigWritten, off);
capNSig_, capOff_);
delete[] buf;
delete[] tRaw; delete[] vRaw;
delete[] tDec; delete[] vDec;
delete[] capBuf_;
capBuf_ = static_cast<uint8 *>(0);
capCap_ = 0u;
capOff_ = 0u;
capNSig_ = 0u;
}
void StreamHub::HandleZoom(const char *json, uint32 slotIdx) {
@@ -1797,8 +1969,7 @@ void StreamHub::HandleZoom(const char *json, uint32 slotIdx) {
/* Read scratch sized for the largest possible ring (no double decimation:
* the whole [t0,t1] slice is read, then LTTB'd once to maxOut). */
const uint32 scratchCap = (ringTemporal_ > ringScalar_) ? ringTemporal_
: ringScalar_;
const uint32 scratchCap = CurrentMaxRingCapacity();
float64 *tRaw = new float64[scratchCap];
float64 *vRaw = new float64[scratchCap];
float64 *tDec = (maxOut > 0u) ? new float64[maxOut] : static_cast<float64 *>(0);