Extract elapsedAtLeast to core/util, dedupe the throttle rationale, and correct the alloc-count/PLAN citations
This commit is contained in:
+12
-8
@@ -850,20 +850,24 @@ the projects where the switch is slowest.
|
|||||||
`enumerateLiveGuids` and their cadence inside `bankPanelRefresh`). **Does not own:** any other
|
`enumerateLiveGuids` and their cadence inside `bankPanelRefresh`). **Does not own:** any other
|
||||||
panel TU, `view.cpp`, or the auto-tag RULES — only how often they are asked.
|
panel TU, `view.cpp`, or the auto-tag RULES — only how often they are asked.
|
||||||
|
|
||||||
**Behavior.** `detectNewContent` (`panel_input.cpp:146`) runs `enumerateLiveGuids` (`:106-135`)
|
**Behavior.** `detectNewContent` (`panel_input.cpp:147`) runs `enumerateLiveGuids` (`:106-135`)
|
||||||
every tick of REAPER's `"timer"` registration (`main.cpp:341`, ~30/sec), regardless of panel
|
every tick of REAPER's `"timer"` registration (`main.cpp:354`, ~30/sec), regardless of panel
|
||||||
state — O(T + I) REAPER string calls plus ~4(T+I) allocations per tick, on every project. Throttle
|
state — O(T + I) REAPER string calls plus roughly 5 allocations per track and 6 per item per
|
||||||
the ENUMERATION, not the detection semantics: the same GUIDs must still be detected, the
|
tick (a per-element-type count, not the earlier `~4(T+I)` derivation, which undercounted; the
|
||||||
first-poll and `reloadPending` re-baseline guards must still fire before any diff, and the
|
ratio the throttle buys is unaffected by the correction). Throttle the ENUMERATION, not the
|
||||||
lane-minting pass must still run only on a tick that tagged. A slower cadence is a latency
|
detection semantics: the same GUIDs must still be detected, the first-poll and `reloadPending`
|
||||||
change to an invisible background tag, and the track states the chosen interval and why.
|
re-baseline guards must still fire before any diff, and the lane-minting pass must still run
|
||||||
|
only on a tick that tagged. A slower cadence is a latency change to an invisible background
|
||||||
|
tag, and the track states the chosen interval and why.
|
||||||
|
|
||||||
**Acceptance criteria.**
|
**Acceptance criteria.**
|
||||||
- New tracks and items are still auto-tagged into the active mode, and a project reload still
|
- New tracks and items are still auto-tagged into the active mode, and a project reload still
|
||||||
re-baselines rather than mass-tagging.
|
re-baselines rather than mass-tagging.
|
||||||
- The Ρ-W1-T1 rule survives: an added GUID that already carries a membership record is still
|
- The Ρ-W1-T1 rule survives: an added GUID that already carries a membership record is still
|
||||||
dropped, so a render-in-place result is not re-tagged to Design.
|
dropped, so a render-in-place result is not re-tagged to Design.
|
||||||
- The per-tick allocation count is measured before and after.
|
- The per-tick allocation count is derived, not measured: it needs live REAPER-side track/item
|
||||||
|
counts and a runtime allocation trap neither CTest nor this repo's tooling provides.
|
||||||
|
`[verify — DAW]`.
|
||||||
- The panel's own repaint cadence is unaffected.
|
- The panel's own repaint cadence is unaffected.
|
||||||
|
|
||||||
**Prerequisites.** None beyond Ω-W1. **Discharges:** Ω.7's polling cost.
|
**Prerequisites.** None beyond Ω-W1. **Discharges:** Ω.7's polling cost.
|
||||||
|
|||||||
@@ -5,3 +5,8 @@ reasampler_test(file_bytes LINK file_bytes)
|
|||||||
add_library(curve_law INTERFACE)
|
add_library(curve_law INTERFACE)
|
||||||
target_include_directories(curve_law INTERFACE ${REASAMPLER_SRC_DIR})
|
target_include_directories(curve_law INTERFACE ${REASAMPLER_SRC_DIR})
|
||||||
reasampler_test(curve_law LINK curve_law)
|
reasampler_test(curve_law LINK curve_law)
|
||||||
|
|
||||||
|
# The GetTickCount()-wraparound-safe throttle-gate test is header-only, hence INTERFACE.
|
||||||
|
add_library(elapsed_at_least INTERFACE)
|
||||||
|
target_include_directories(elapsed_at_least INTERFACE ${REASAMPLER_SRC_DIR})
|
||||||
|
reasampler_test(elapsed_at_least LINK elapsed_at_least)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
// elapsed_at_least — the ONE "has at least this long passed" test behind every
|
||||||
|
// GetTickCount()-style throttle gate (panel_input's detect cadence, panel_drag's tooltip
|
||||||
|
// delay). Unsigned subtraction is the deliberate implementation, not an oversight: it is
|
||||||
|
// exactly correct across a GetTickCount() wraparound (~49.7 days uptime) because `now -
|
||||||
|
// since`, computed mod 2^32, IS the true forward millisecond distance even when `now`'s
|
||||||
|
// raw value is numerically less than `since`'s. A signed or `now < since` guard would
|
||||||
|
// instead wedge the gate shut for up to the wrap period.
|
||||||
|
|
||||||
|
namespace reasampler::util {
|
||||||
|
|
||||||
|
// True once at least `interval` ms separate `since` from `now` (both GetTickCount()-style
|
||||||
|
// millisecond counters). `since == 0, interval == 0` reads true, so a zero-initialized
|
||||||
|
// "last fired" tick opens the gate on its very first call.
|
||||||
|
inline bool elapsedAtLeast(unsigned int now, unsigned int since, unsigned int interval) {
|
||||||
|
return now - since >= interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::util
|
||||||
@@ -322,7 +322,7 @@ void maybeShowTooltip() {
|
|||||||
const HoverKind k = g_panel.hovered.kind;
|
const HoverKind k = g_panel.hovered.kind;
|
||||||
if (k != HoverKind::TopBarButton && k != HoverKind::BottomBarButton) return;
|
if (k != HoverKind::TopBarButton && k != HoverKind::BottomBarButton) return;
|
||||||
const unsigned int now = GetTickCount();
|
const unsigned int now = GetTickCount();
|
||||||
if (now - g_panel.hoverSinceTick >= kTooltipDelayMs) {
|
if (elapsedAtLeast(now, g_panel.hoverSinceTick, kTooltipDelayMs)) {
|
||||||
g_panel.tooltipShown = true;
|
g_panel.tooltipShown = true;
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
|||||||
|
|
||||||
// One detection tick: REAPER exposes no "item/track added" callback, so this diffs live
|
// One detection tick: REAPER exposes no "item/track added" callback, so this diffs live
|
||||||
// GUIDs against the baseline and auto-tags the new ones into the active mode. Called every
|
// GUIDs against the baseline and auto-tags the new ones into the active mode. Called every
|
||||||
// timer tick regardless of panel open/close, but the enumeration itself is throttled to
|
// timer tick regardless of panel open/close; the enumeration's throttle and its correctness
|
||||||
// kDetectIntervalMs (see the gate below). READ-ONLY on the project; mutates only the
|
// argument are the gate immediately below. READ-ONLY on the project; mutates only the
|
||||||
// in-memory membership index — deliberately OUTSIDE any Undo block (auto-tag is a
|
// in-memory membership index — deliberately OUTSIDE any Undo block (auto-tag is a
|
||||||
// background metadata update, not a destructive edit; an Undo block here would flood
|
// background metadata update, not a destructive edit; an Undo block here would flood
|
||||||
// REAPER's history with an entry per tick that sees new content).
|
// REAPER's history with an entry per tick that sees new content).
|
||||||
@@ -147,12 +147,18 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
|||||||
bool detectNewContent() {
|
bool detectNewContent() {
|
||||||
if (!g_panel.session) return false;
|
if (!g_panel.session) return false;
|
||||||
|
|
||||||
// Throttles the enumeration (O(T+I) REAPER calls + allocations) to kDetectIntervalMs,
|
// Throttles the enumeration (O(T+I) REAPER calls + allocations) to kDetectIntervalMs via
|
||||||
// independent of how often the caller ticks. A skipped tick leaves reloadPending/the
|
// elapsedAtLeast (core/util; wraparound-safe against GetTickCount()'s rollover, see its
|
||||||
// baseline untouched, so the guards below still run before the NEXT diff whenever this
|
// header). A skipped tick leaves reloadPending/the baseline untouched, so the guards below
|
||||||
// gate next opens — only the diff's cadence changes, not its correctness.
|
// still run before the NEXT diff whenever this gate next opens — only the diff's cadence
|
||||||
|
// changes, not its correctness.
|
||||||
|
//
|
||||||
|
// KNOWN CONSEQUENCE, not new: a new item's mode resolves from
|
||||||
|
// preExistingTrackModes()/activeModeId() at ENUMERATION time (below), not creation time, so
|
||||||
|
// switching mode or moving the item before the next tick can land it in a different mode
|
||||||
|
// than a tighter cadence would — the window existed at ~33 ms; now widened ~16x.
|
||||||
const unsigned int now = GetTickCount();
|
const unsigned int now = GetTickCount();
|
||||||
if (now - g_panel.lastDetectTick < kDetectIntervalMs) return false;
|
if (!elapsedAtLeast(now, g_panel.lastDetectTick, kDetectIntervalMs)) return false;
|
||||||
g_panel.lastDetectTick = now;
|
g_panel.lastDetectTick = now;
|
||||||
|
|
||||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||||
@@ -556,10 +562,10 @@ void bankPanelNotifyProjectLoaded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void bankPanelRefresh() {
|
void bankPanelRefresh() {
|
||||||
// New-content auto-tag detection is CALLED every tick regardless of panel open/close
|
// New-content auto-tag detection runs every tick regardless of panel open/close (tracks/
|
||||||
// (tracks/items are created in the arrange view, not the panel), but the enumeration
|
// items are created in the arrange view, not the panel); detectNewContent (panel_input.cpp,
|
||||||
// it drives is throttled — see kDetectIntervalMs. READ-ONLY on the project; only
|
// above) is the authoritative comment for its throttle, correctness argument, and
|
||||||
// mutates the in-memory membership index.
|
// read-only/mutation contract.
|
||||||
const bool tagged = panel::detectNewContent();
|
const bool tagged = panel::detectNewContent();
|
||||||
|
|
||||||
// Lane minting runs ONLY when detection just tagged new content — a track can only
|
// Lane minting runs ONLY when detection just tagged new content — a track can only
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
#include "core/ui/tab_strip.h"
|
#include "core/ui/tab_strip.h"
|
||||||
#include "core/ui/theme.h"
|
#include "core/ui/theme.h"
|
||||||
#include "core/ui/tooltip.h"
|
#include "core/ui/tooltip.h"
|
||||||
|
#include "core/util/elapsed_at_least.h"
|
||||||
#include "core/version/app_version.h"
|
#include "core/version/app_version.h"
|
||||||
#include "core/view/guid_diff.h"
|
#include "core/view/guid_diff.h"
|
||||||
#include "core/view/lane_keys.h"
|
#include "core/view/lane_keys.h"
|
||||||
@@ -182,6 +183,9 @@ using version::channelCommandId;
|
|||||||
using version::dockIdent;
|
using version::dockIdent;
|
||||||
using version::dockTitle;
|
using version::dockTitle;
|
||||||
|
|
||||||
|
// core/util
|
||||||
|
using util::elapsedAtLeast;
|
||||||
|
|
||||||
// core/wire
|
// core/wire
|
||||||
using wire::buildInstrumentDropPreset;
|
using wire::buildInstrumentDropPreset;
|
||||||
|
|
||||||
@@ -208,9 +212,8 @@ inline constexpr int kTooltipTextH = 14;
|
|||||||
|
|
||||||
// New-content detection's enumeration cadence — matches the VST3 side's own
|
// New-content detection's enumeration cadence — matches the VST3 side's own
|
||||||
// kSyncTimerIntervalMs (editor_platform.cpp), this codebase's established interval for a
|
// kSyncTimerIntervalMs (editor_platform.cpp), this codebase's established interval for a
|
||||||
// background poll nothing visible depends on. Auto-tag is an invisible metadata update
|
// background poll nothing visible depends on. The throttle's correctness argument lives at
|
||||||
// (see detectNewContent), so a 15x cadence cut (30/s -> 2/s) costs latency no one watches
|
// detectNewContent's own comment (panel_input.cpp) — the authoritative home; not restated here.
|
||||||
// for, not correctness.
|
|
||||||
inline constexpr unsigned int kDetectIntervalMs = 500;
|
inline constexpr unsigned int kDetectIntervalMs = 500;
|
||||||
|
|
||||||
// Client area top to bottom: top toolbar | split body | bottom toolbar | footer.
|
// Client area top to bottom: top toolbar | split body | bottom toolbar | footer.
|
||||||
@@ -362,7 +365,10 @@ struct PanelState {
|
|||||||
|
|
||||||
// Throttles enumerateLiveGuids to kDetectIntervalMs regardless of how often
|
// Throttles enumerateLiveGuids to kDetectIntervalMs regardless of how often
|
||||||
// bankPanelRefresh itself is called (the OnTimer poll, ~30/s, plus a handful of
|
// bankPanelRefresh itself is called (the OnTimer poll, ~30/s, plus a handful of
|
||||||
// post-capture/ingest/bake call sites). 0 forces the very first tick through.
|
// post-capture/ingest/bake call sites); the gate's correctness argument lives at
|
||||||
|
// detectNewContent (panel_input.cpp), not restated here. 0 forces the very first tick
|
||||||
|
// through PROVIDED system uptime is already >= kDetectIntervalMs at that call — always
|
||||||
|
// true in practice, but an assumption elapsedAtLeast takes, not one it guarantees.
|
||||||
unsigned int lastDetectTick = 0;
|
unsigned int lastDetectTick = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Standalone tests for reasampler::util::elapsedAtLeast — no VST3, no REAPER, no framework.
|
||||||
|
// Same fast assert loop as the sibling pure tests.
|
||||||
|
//
|
||||||
|
// Covers: the ordinary in-range case; the exact boundary (elapsed == interval opens the
|
||||||
|
// gate, elapsed == interval - 1 does not); the zero/zero first-call case; and the
|
||||||
|
// GetTickCount() WRAPAROUND — the one property in this idiom a reader has to think about.
|
||||||
|
// A `now` that has numerically wrapped past `since` must still open the gate once the true
|
||||||
|
// forward elapsed time reaches `interval`, never wedge it shut.
|
||||||
|
|
||||||
|
#include "../src/core/util/elapsed_at_least.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
using namespace reasampler::util;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
static void testOrdinaryCase() {
|
||||||
|
CHECK(elapsedAtLeast(1000u, 0u, 500u)); // well past the interval
|
||||||
|
CHECK(!elapsedAtLeast(400u, 0u, 500u)); // not there yet
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExactBoundary() {
|
||||||
|
CHECK(elapsedAtLeast(500u, 0u, 500u)); // exactly the interval: gate opens
|
||||||
|
CHECK(!elapsedAtLeast(499u, 0u, 500u)); // one ms short: gate stays shut
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testZeroZeroFirstCallOpensImmediately() {
|
||||||
|
// A zero-initialized "last fired" tick (lastDetectTick's documented default) must not
|
||||||
|
// block the very first call.
|
||||||
|
CHECK(elapsedAtLeast(0u, 0u, 0u));
|
||||||
|
CHECK(elapsedAtLeast(1u, 0u, 0u));
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTickCount() wraps every ~49.7 days (2^32 ms). `since` sits near the top of the range;
|
||||||
|
// `now` has wrapped back around to a small value. Unsigned subtraction computes the correct
|
||||||
|
// forward distance (the gap to UINT_MAX plus the distance past zero) rather than reading as
|
||||||
|
// a huge negative gap that a signed/naive comparison would misinterpret as "not yet".
|
||||||
|
static void testWraparoundOpensRatherThanWedgesShut() {
|
||||||
|
const unsigned int kMax = std::numeric_limits<unsigned int>::max();
|
||||||
|
const unsigned int since = kMax - 49u; // 50 ms of headroom before the counter wraps
|
||||||
|
const unsigned int interval = 500u;
|
||||||
|
|
||||||
|
// 450 ms after the wrap: 51 ms (headroom to wrap) + 450 ms = 501 ms true elapsed -> open.
|
||||||
|
CHECK(elapsedAtLeast(450u, since, interval));
|
||||||
|
// 439 ms after the wrap: 51 + 439 = 490 ms true elapsed -> still shut.
|
||||||
|
CHECK(!elapsedAtLeast(439u, since, interval));
|
||||||
|
// Exactly at the wrap instant (now == 0): 51 ms true elapsed -> shut.
|
||||||
|
CHECK(!elapsedAtLeast(0u, since, interval));
|
||||||
|
// now == since (no time passed, mid-wrap-approach): never wedges into "always open".
|
||||||
|
CHECK(!elapsedAtLeast(since, since, interval));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testOrdinaryCase();
|
||||||
|
testExactBoundary();
|
||||||
|
testZeroZeroFirstCallOpensImmediately();
|
||||||
|
testWraparoundOpensRatherThanWedgesShut();
|
||||||
|
if (g_fail == 0) std::printf("elapsed_at_least: all tests passed\n");
|
||||||
|
else std::printf("elapsed_at_least: %d FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user