// TuneAdvisor / TuningManager reconciliation + pool config + ETA calculation tests (Catch2) // Tests three areas: // Group A: Post-ingest budget reconciliation and distribution correctness // Group B: Pool configuration minimum guards // Group C: ETA remaining calculation accuracy // // Uses same TDD pattern as tuning_allocation_catch2_test.cpp. #include #include #include #include #include "../../common/env_compat.h" #include "../../common/test_helpers_catch2.h" #include #include #include #include #include #include using namespace yams::daemon; namespace { class ProfileGuard { TuneAdvisor::Profile prev_; public: explicit ProfileGuard(TuneAdvisor::Profile profile) : prev_(TuneAdvisor::tuningProfile()) { TuneAdvisor::setTuningProfile(profile); } ~ProfileGuard() { TuneAdvisor::setTuningProfile(prev_); } ProfileGuard(const ProfileGuard&) = delete; ProfileGuard& operator=(const ProfileGuard&) = delete; }; using yams::test::ScopedEnvVar; class HwGuard { unsigned prev_; public: explicit HwGuard(unsigned hw) : prev_(TuneAdvisor::hardwareConcurrency()) { TuneAdvisor::setHardwareConcurrencyForTests(hw); } ~HwGuard() { TuneAdvisor::setHardwareConcurrencyForTests(prev_); } HwGuard(const HwGuard&) = delete; HwGuard& operator=(const HwGuard&) = delete; }; /// Reset all per-stage overrides to 0 (no override) and dynamic caps to UINT32_MAX (unset) void resetPostIngestOverrides() { TuneAdvisor::setPostExtractionConcurrent(0); TuneAdvisor::setPostKgConcurrent(0); TuneAdvisor::setPostSymbolConcurrent(0); TuneAdvisor::setPostEntityConcurrent(0); TuneAdvisor::setPostTitleConcurrent(0); TuneAdvisor::setPostEmbedConcurrent(0); TuneAdvisor::setPostExtractionConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostKgConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostSymbolConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostEntityConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostTitleConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostEmbedConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostIngestTotalConcurrent(0); } void setAllPostIngestStagesActive() { TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Extraction, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::KnowledgeGraph, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Symbol, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Entity, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Title, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Embed, true); } /// Helper: sum all per-stage post-ingest concurrent values uint32_t sumAllStages() { return TuneAdvisor::postExtractionConcurrent() + TuneAdvisor::postKgConcurrent() + TuneAdvisor::postSymbolConcurrent() + TuneAdvisor::postEntityConcurrent() + TuneAdvisor::postTitleConcurrent() + TuneAdvisor::postEmbedConcurrent(); } } // namespace TEST_CASE("ONNX slot budget preserves shared capacity", "[daemon][tune][onnx][reconciliation][catch2]") { SECTION("Normal operation expands the pool for all requested reservations") { const auto budget = computeOnnxSlotBudget( /*desiredMax=*/4, /*glinerReserved=*/2, /*embedReserved=*/3, /*rerankerReserved=*/1, /*pressureCap=*/2, /*underPressure=*/false); CHECK(budget.maxSlots == 7); CHECK(budget.glinerReserved == 2); CHECK(budget.embedReserved == 3); CHECK(budget.rerankerReserved == 1); } SECTION("Pressure keeps one shared slot and trims reservations in priority order") { const auto budget = computeOnnxSlotBudget( /*desiredMax=*/12, /*glinerReserved=*/2, /*embedReserved=*/4, /*rerankerReserved=*/3, /*pressureCap=*/5, /*underPressure=*/true); CHECK(budget.maxSlots == 5); CHECK(budget.glinerReserved == 2); CHECK(budget.embedReserved == 2); CHECK(budget.rerankerReserved == 0); } SECTION("Registry bounds hold for extreme inputs") { const auto budget = computeOnnxSlotBudget( /*desiredMax=*/100, /*glinerReserved=*/100, /*embedReserved=*/100, /*rerankerReserved=*/100, /*pressureCap=*/1, /*underPressure=*/true); CHECK(budget.maxSlots == 2); CHECK(budget.glinerReserved == 1); CHECK(budget.embedReserved == 0); CHECK(budget.rerankerReserved == 0); } } // ============================================================================= // Group A: Post-ingest budget reconciliation and distribution correctness // ============================================================================= TEST_CASE("Post-ingest budget distribution correctness", "[daemon][tune][reconciliation][catch2]") { SECTION("All active stages get >= 1 when budget allows") { // With 6 active stages and budget >= 6, every stage should get at least 1. resetPostIngestOverrides(); setAllPostIngestStagesActive(); ScopedEnvVar envPostIngest("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); ScopedEnvVar envMaxThreads("YAMS_MAX_THREADS", "0"); for (unsigned hw : {8u, 12u, 16u, 24u, 32u}) { HwGuard hwGuard(hw); for (auto profile : {TuneAdvisor::Profile::Efficient, TuneAdvisor::Profile::Balanced, TuneAdvisor::Profile::Aggressive}) { ProfileGuard profileGuard(profile); uint32_t budget = TuneAdvisor::postIngestTotalConcurrent(); if (budget < 6) continue; // Only test when budget can cover all stages uint32_t ext = TuneAdvisor::postExtractionConcurrent(); uint32_t kg = TuneAdvisor::postKgConcurrent(); uint32_t sym = TuneAdvisor::postSymbolConcurrent(); uint32_t ent = TuneAdvisor::postEntityConcurrent(); uint32_t tit = TuneAdvisor::postTitleConcurrent(); uint32_t emb = TuneAdvisor::postEmbedConcurrent(); INFO("hw=" << hw << " profile=" << static_cast(profile) << " budget=" << budget); INFO("ext=" << ext << " kg=" << kg << " sym=" << sym << " ent=" << ent << " tit=" << tit << " emb=" << emb); CHECK(ext >= 1); CHECK(kg >= 1); CHECK(sym >= 1); CHECK(ent >= 1); CHECK(tit >= 1); CHECK(emb >= 1); } } } SECTION("Sum of per-stage allocations does not exceed budget") { resetPostIngestOverrides(); setAllPostIngestStagesActive(); ScopedEnvVar envPostIngest("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); ScopedEnvVar envMaxThreads("YAMS_MAX_THREADS", "0"); for (unsigned hw : {2u, 4u, 8u, 16u, 32u}) { HwGuard hwGuard(hw); for (auto profile : {TuneAdvisor::Profile::Efficient, TuneAdvisor::Profile::Balanced, TuneAdvisor::Profile::Aggressive}) { ProfileGuard profileGuard(profile); uint32_t budget = TuneAdvisor::postIngestTotalConcurrent(); uint32_t sum = sumAllStages(); INFO("hw=" << hw << " profile=" << static_cast(profile) << " budget=" << budget << " sum=" << sum); CHECK(sum <= budget); } } } SECTION("Extraction and embed get minimum 1 when budget >= 2") { resetPostIngestOverrides(); setAllPostIngestStagesActive(); ScopedEnvVar envPostIngest("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); ScopedEnvVar envMaxThreads("YAMS_MAX_THREADS", "0"); for (unsigned hw : {2u, 3u, 4u}) { HwGuard hwGuard(hw); for (auto profile : {TuneAdvisor::Profile::Efficient, TuneAdvisor::Profile::Balanced, TuneAdvisor::Profile::Aggressive}) { ProfileGuard profileGuard(profile); uint32_t budget = TuneAdvisor::postIngestTotalConcurrent(); uint32_t ext = TuneAdvisor::postExtractionConcurrent(); uint32_t emb = TuneAdvisor::postEmbedConcurrent(); INFO("hw=" << hw << " profile=" << static_cast(profile) << " budget=" << budget); if (budget >= 2) { CHECK(ext >= 1); CHECK(emb >= 1); } } } } SECTION("Embed gets higher weight than individual other stages") { // Embed has weight 2 vs weight 1 for others; with sufficient budget, // embed should get at least as much as any single other stage. resetPostIngestOverrides(); setAllPostIngestStagesActive(); ScopedEnvVar envPostIngest("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); ScopedEnvVar envMaxThreads("YAMS_MAX_THREADS", "0"); for (unsigned hw : {16u, 24u, 32u}) { HwGuard hwGuard(hw); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); uint32_t emb = TuneAdvisor::postEmbedConcurrent(); uint32_t kg = TuneAdvisor::postKgConcurrent(); uint32_t sym = TuneAdvisor::postSymbolConcurrent(); INFO("hw=" << hw << " emb=" << emb << " kg=" << kg << " sym=" << sym); CHECK(emb >= kg); CHECK(emb >= sym); } } } TEST_CASE("Contention-aware budget adjustment logic", "[daemon][tune][reconciliation][contention][catch2]") { SECTION("Severe contention drops budget aggressively") { const int32_t adjust = TuningManager::testing_computeContentionBudgetAdjustment( /*waitingRequests=*/3, /*waitMicrosDelta=*/25000, /*timeoutDelta=*/1, /*failedDelta=*/0, /*processedDelta=*/6, /*healthyTicks=*/0); CHECK(adjust == -2); } SECTION("Moderate contention applies gentle reduction") { const int32_t adjust = TuningManager::testing_computeContentionBudgetAdjustment( /*waitingRequests=*/2, /*waitMicrosDelta=*/9000, /*timeoutDelta=*/0, /*failedDelta=*/0, /*processedDelta=*/6, /*healthyTicks=*/0); CHECK(adjust == -1); } SECTION("Healthy sustained throughput allows gradual increase") { const int32_t adjust = TuningManager::testing_computeContentionBudgetAdjustment( /*waitingRequests=*/0, /*waitMicrosDelta=*/500, /*timeoutDelta=*/0, /*failedDelta=*/0, /*processedDelta=*/6, /*healthyTicks=*/2); CHECK(adjust == 1); } SECTION("Insufficient healthy history keeps budget steady") { const int32_t adjust = TuningManager::testing_computeContentionBudgetAdjustment( /*waitingRequests=*/0, /*waitMicrosDelta=*/500, /*timeoutDelta=*/0, /*failedDelta=*/0, /*processedDelta=*/6, /*healthyTicks=*/1); CHECK(adjust == 0); } } // ============================================================================= // Group A2: Active mask filtering // ============================================================================= TEST_CASE("Post-ingest active mask filters stages correctly", "[daemon][tune][reconciliation][catch2]") { SECTION("Disabled stages get 0 allocation") { resetPostIngestOverrides(); ScopedEnvVar envPostIngest("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); ScopedEnvVar envMaxThreads("YAMS_MAX_THREADS", "0"); HwGuard hwGuard(16); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); // Enable only extraction and embed (disable kg, symbol, entity, title) TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Extraction, true); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::KnowledgeGraph, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Symbol, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Entity, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Title, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Embed, true); uint32_t kg = TuneAdvisor::postKgConcurrent(); uint32_t sym = TuneAdvisor::postSymbolConcurrent(); uint32_t ent = TuneAdvisor::postEntityConcurrent(); uint32_t tit = TuneAdvisor::postTitleConcurrent(); uint32_t ext = TuneAdvisor::postExtractionConcurrent(); uint32_t emb = TuneAdvisor::postEmbedConcurrent(); CHECK(kg == 0); CHECK(sym == 0); CHECK(ent == 0); CHECK(tit == 0); CHECK(ext >= 1); CHECK(emb >= 1); // Budget redistributed to enabled stages — they should get more // than when all 6 are active setAllPostIngestStagesActive(); uint32_t extAll = TuneAdvisor::postExtractionConcurrent(); uint32_t embAll = TuneAdvisor::postEmbedConcurrent(); INFO("ext(2 active)=" << ext << " ext(all active)=" << extAll); INFO("emb(2 active)=" << emb << " emb(all active)=" << embAll); CHECK(ext >= extAll); CHECK(emb >= embAll); } SECTION("Disabling all stages reduces total budget") { resetPostIngestOverrides(); HwGuard hwGuard(8); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); uint32_t budgetAllActive = TuneAdvisor::postIngestTotalConcurrent(); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Extraction, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::KnowledgeGraph, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Symbol, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Entity, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Title, false); TuneAdvisor::setPostIngestStageActive(TuneAdvisor::PostIngestStage::Embed, false); // When all disabled, the total budget floor clamps at 0 active stages // but per-stage getters still return their computed values (the active // mask only affects the total budget, not individual allocations). uint32_t budgetNoneActive = TuneAdvisor::postIngestTotalConcurrent(); INFO("budget(all active)=" << budgetAllActive << " budget(none active)=" << budgetNoneActive); CHECK(budgetNoneActive <= budgetAllActive); // Restore for subsequent tests setAllPostIngestStagesActive(); } } // ============================================================================= // Group A3: Per-stage override clamping // ============================================================================= TEST_CASE("Per-stage overrides are respected and clamped", "[daemon][tune][reconciliation][catch2]") { SECTION("Override takes precedence over computed allocation") { resetPostIngestOverrides(); setAllPostIngestStagesActive(); HwGuard hwGuard(16); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); // Set a specific override for extraction TuneAdvisor::setPostExtractionConcurrent(5); uint32_t ext = TuneAdvisor::postExtractionConcurrent(); CHECK(ext == 5); // Reset TuneAdvisor::setPostExtractionConcurrent(0); } SECTION("Override is clamped to max cap") { resetPostIngestOverrides(); setAllPostIngestStagesActive(); HwGuard hwGuard(16); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); // Entity max cap is 16 — setting beyond should be clamped TuneAdvisor::setPostEntityConcurrent(16); uint32_t ent = TuneAdvisor::postEntityConcurrent(); INFO("entity with override=16: " << ent); CHECK(ent <= 16); // Reset TuneAdvisor::setPostEntityConcurrent(0); } SECTION("Overrides are independent — sum may exceed budget (no runtime clamping)") { resetPostIngestOverrides(); setAllPostIngestStagesActive(); HwGuard hwGuard(8); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); // TuneAdvisor per-stage overrides are not clamped to the total budget. // The TuningManager reconciliation loop is responsible for capping at runtime. // Verify overrides take effect individually. TuneAdvisor::setPostExtractionConcurrent(3); TuneAdvisor::setPostKgConcurrent(3); TuneAdvisor::setPostEmbedConcurrent(3); CHECK(TuneAdvisor::postExtractionConcurrent() == 3); CHECK(TuneAdvisor::postKgConcurrent() == 3); CHECK(TuneAdvisor::postEmbedConcurrent() == 3); uint32_t budget = TuneAdvisor::postIngestTotalConcurrent(); uint32_t sum = sumAllStages(); INFO("budget=" << budget << " sum=" << sum); // Sum CAN exceed budget — that's by design; TuningManager reconciles at runtime CHECK(sum >= 9); // at least the 3 overridden stages resetPostIngestOverrides(); } } // ============================================================================= // Group B: Pool configuration minimum guards // ============================================================================= TEST_CASE("Pool configuration defaults are reasonable", "[daemon][tune][reconciliation][pool][catch2]") { SECTION("poolMinSizeIpc defaults to at least 1") { uint32_t minIpc = TuneAdvisor::poolMinSizeIpc(); INFO("poolMinSizeIpc default=" << minIpc); CHECK(minIpc >= 1); } SECTION("poolMinSizeIpcIo defaults to at least 1") { uint32_t minIo = TuneAdvisor::poolMinSizeIpcIo(); INFO("poolMinSizeIpcIo default=" << minIo); CHECK(minIo >= 1); } SECTION("poolMaxSizeIpc is greater than poolMinSizeIpc") { uint32_t minIpc = TuneAdvisor::poolMinSizeIpc(); uint32_t maxIpc = TuneAdvisor::poolMaxSizeIpc(); INFO("IPC pool: min=" << minIpc << " max=" << maxIpc); CHECK(maxIpc > minIpc); } SECTION("poolMaxSizeIpcIo is greater than poolMinSizeIpcIo") { uint32_t minIo = TuneAdvisor::poolMinSizeIpcIo(); uint32_t maxIo = TuneAdvisor::poolMaxSizeIpcIo(); INFO("IO pool: min=" << minIo << " max=" << maxIo); CHECK(maxIo > minIo); } SECTION("IO pool min_size should be at least 2 after guard") { // This tests the ServiceManager guard: IO pool should have min >= 2. // The raw TuneAdvisor default is 1, but ServiceManager should guard it. // We test the expected post-guard value here. uint32_t rawMin = TuneAdvisor::poolMinSizeIpcIo(); uint32_t guardedMin = rawMin < 2 ? 2 : rawMin; INFO("raw min=" << rawMin << " guarded min=" << guardedMin); CHECK(guardedMin >= 2); } SECTION("recommendedThreads never returns 0") { for (unsigned hw : {1u, 2u, 4u}) { HwGuard hwGuard(hw); for (auto profile : {TuneAdvisor::Profile::Efficient, TuneAdvisor::Profile::Balanced, TuneAdvisor::Profile::Aggressive}) { ProfileGuard profileGuard(profile); uint32_t threads = TuneAdvisor::recommendedThreads(0.5); INFO("hw=" << hw << " profile=" << static_cast(profile) << " recommendedThreads(0.5)=" << threads); CHECK(threads >= 1); } } } } // ============================================================================= // Group C: ETA remaining calculation accuracy // ============================================================================= TEST_CASE("ETA remaining calculation handles integer truncation", "[daemon][tune][reconciliation][eta][catch2]") { // Test the ETA formula: remain = max(0, exp - (exp * progress + 99) / 100) // The old formula was: remain = max(0, exp - (exp * progress) / 100) // which suffers from integer truncation for small exp values. auto computeEtaRemaining = [](int exp, int progress) -> int { return ServiceManager::computeEtaRemaining(exp, progress); }; SECTION("Zero progress gives full ETA") { CHECK(computeEtaRemaining(5, 0) == 5); CHECK(computeEtaRemaining(1, 0) == 1); CHECK(computeEtaRemaining(20, 0) == 20); } SECTION("100% progress gives 0 ETA") { CHECK(computeEtaRemaining(5, 100) == 0); CHECK(computeEtaRemaining(1, 100) == 0); CHECK(computeEtaRemaining(20, 100) == 0); } SECTION("50% progress on small exp should reduce ETA") { // With exp=1, progress=50: old formula gives (1*50)/100=0, remain=1 // Fixed formula should give remain <= 1 (ideally 0, since half is done) int remain = computeEtaRemaining(1, 50); INFO("exp=1, progress=50, remain=" << remain); CHECK(remain <= 1); CHECK(remain >= 0); } SECTION("75% progress on small exp should reduce ETA significantly") { // With exp=1, progress=75: old formula gives (1*75)/100=0, remain=1 // Fixed formula should give 0 int remain = computeEtaRemaining(1, 75); INFO("exp=1, progress=75, remain=" << remain); CHECK(remain == 0); } SECTION("ETA is monotonically non-increasing as progress increases") { for (int exp : {1, 2, 3, 5, 10, 20}) { int prev = exp; for (int progress = 0; progress <= 100; progress += 10) { int remain = computeEtaRemaining(exp, progress); INFO("exp=" << exp << " progress=" << progress << " remain=" << remain << " prev=" << prev); CHECK(remain <= prev); CHECK(remain >= 0); prev = remain; } } } SECTION("Normal sized exp values are accurate") { // exp=5, progress=50: should be about 2-3 int r1 = computeEtaRemaining(5, 50); CHECK(r1 >= 2); CHECK(r1 <= 3); // exp=10, progress=30: should be about 7 int r2 = computeEtaRemaining(10, 30); CHECK(r2 >= 6); CHECK(r2 <= 7); // exp=20, progress=90: should be about 2 int r3 = computeEtaRemaining(20, 90); CHECK(r3 >= 1); CHECK(r3 <= 2); } SECTION("Negative and edge cases are safe") { CHECK(computeEtaRemaining(0, 0) == 0); CHECK(computeEtaRemaining(0, 50) == 0); CHECK(computeEtaRemaining(0, 100) == 0); // Progress beyond 100 should still be safe CHECK(computeEtaRemaining(5, 150) == 0); } } // ============================================================================= // Group D: Embed scale bias under backlog pressure // ============================================================================= TEST_CASE("Embed scale bias ramps with queue pressure", "[daemon][tune][reconciliation][embed-bias][catch2]") { SECTION("Bias increases as embed queue grows") { const auto b0 = TuningManager::testing_computeEmbedScaleBias(0, 0, 0, 0); const auto b1 = TuningManager::testing_computeEmbedScaleBias(200, 0, 0, 0); const auto b2 = TuningManager::testing_computeEmbedScaleBias(800, 0, 0, 0); const auto b3 = TuningManager::testing_computeEmbedScaleBias(2200, 0, 0, 0); CHECK(b1 >= b0); CHECK(b2 >= b1); CHECK(b3 >= b2); } SECTION("Drops and saturation increase bias beyond pure queue level") { const auto queueOnly = TuningManager::testing_computeEmbedScaleBias(900, 0, 0, 0); const auto withDrops = TuningManager::testing_computeEmbedScaleBias(900, 50, 0, 0); const auto withSaturation = TuningManager::testing_computeEmbedScaleBias(900, 50, 900, 1); CHECK(withDrops >= queueOnly); CHECK(withSaturation >= withDrops); } SECTION("Bias is bounded to avoid runaway oversubscription") { const auto capped = TuningManager::testing_computeEmbedScaleBias(100000, 100000, 100000, 8); CHECK(capped <= 8); } } TEST_CASE("Queue-pressure rebalance shifts capacity to backlogged stages", "[daemon][tune][reconciliation][queue][catch2]") { SECTION("Shifts one slot from low-pressure to high-pressure stage") { std::array targets{2, 2, 2, 2, 2, 2}; std::array floors{1, 0, 0, 0, 0, 0}; std::array depths{0, 400, 1, 1, 1, 1}; std::array active{true, true, true, true, true, true}; TuningManager::testing_rebalanceTargetsByQueue(targets, floors, depths, active); // KG backlog dominates, so KG should gain capacity. CHECK(targets[1] > 2); // Total budget must be preserved. CHECK(targets[0] + targets[1] + targets[2] + targets[3] + targets[4] + targets[5] == 12); } SECTION("Does not violate configured floors") { std::array targets{1, 3, 1, 1, 1, 1}; std::array floors{1, 0, 0, 0, 0, 0}; std::array depths{0, 0, 0, 0, 0, 500}; std::array active{true, true, true, true, true, true}; TuningManager::testing_rebalanceTargetsByQueue(targets, floors, depths, active); CHECK(targets[0] >= floors[0]); CHECK(targets[5] > 1); CHECK(targets[0] + targets[1] + targets[2] + targets[3] + targets[4] + targets[5] == 8); } } TEST_CASE("Post-ingest scale test mode isolates zero-target policy", "[daemon][tune][reconciliation][scale-mode][catch2]") { class ScaleModeGuard { public: ScaleModeGuard() : previous_(TuningManager::testing_postIngestScaleTestMode()) {} ~ScaleModeGuard() { TuningManager::testing_setPostIngestScaleTestMode(previous_); } private: TuningManager::PostIngestScaleTestMode previous_; } guard; SECTION("Normal mode allows zero only when daemon is idle and not busy") { TuningManager::testing_setPostIngestScaleTestMode( TuningManager::PostIngestScaleTestMode::Normal); CHECK(TuningManager::testing_shouldAllowZeroPostIngestTargets(true, false)); CHECK_FALSE(TuningManager::testing_shouldAllowZeroPostIngestTargets(false, false)); CHECK_FALSE(TuningManager::testing_shouldAllowZeroPostIngestTargets(true, true)); CHECK_FALSE(TuningManager::testing_shouldAllowZeroPostIngestTargets(false, true)); } SECTION("ForceBusy mode disables zero-target scale-down") { TuningManager::testing_setPostIngestScaleTestMode( TuningManager::PostIngestScaleTestMode::ForceBusy); CHECK_FALSE(TuningManager::testing_shouldAllowZeroPostIngestTargets(true, false)); CHECK_FALSE(TuningManager::testing_shouldAllowZeroPostIngestTargets(false, false)); CHECK_FALSE(TuningManager::testing_shouldAllowZeroPostIngestTargets(true, true)); } SECTION("ForceIdle mode enables zero-target scale-down") { TuningManager::testing_setPostIngestScaleTestMode( TuningManager::PostIngestScaleTestMode::ForceIdle); CHECK(TuningManager::testing_shouldAllowZeroPostIngestTargets(true, false)); CHECK(TuningManager::testing_shouldAllowZeroPostIngestTargets(false, false)); CHECK(TuningManager::testing_shouldAllowZeroPostIngestTargets(true, true)); CHECK(TuningManager::testing_shouldAllowZeroPostIngestTargets(false, true)); } } // ============================================================================= // Group E: Deterministic tuning simulation with GradientLimiter // ============================================================================= TEST_CASE("Embed tuning simulation improves queue control with gradient limiter", "[daemon][tune][reconciliation][gradient][sim][!benchmark]") { struct SimResult { std::size_t peakQueue{0}; std::size_t finalQueue{0}; std::size_t totalProcessed{0}; double avgTarget{0.0}; }; auto runSim = [&](bool useDynamicBias) -> SimResult { GradientLimiter::Config cfg; cfg.initialLimit = 4.0; cfg.minLimit = 1.0; cfg.maxLimit = 32.0; cfg.warmupSamples = 3; cfg.smoothingAlpha = 0.5; cfg.longWindowAlpha = 0.1; cfg.tolerance = 1.5; GradientLimiter limiter("embed-sim", cfg); constexpr std::size_t kTicks = 180; constexpr uint32_t kBaseTarget = 2; constexpr std::size_t kPerWorkerRate = 22; constexpr std::size_t kDropThreshold = 1100; std::size_t queue = 0; std::size_t peakQueue = 0; std::size_t totalProcessed = 0; std::size_t targetAccumulator = 0; for (std::size_t tick = 0; tick < kTicks; ++tick) { std::size_t arrivals = 0; if (tick < 40) { arrivals = 35; // warm ramp } else if (tick < 110) { arrivals = 70; // sustained burst } else { arrivals = 18; // recovery tail } const uint64_t droppedDelta = queue > kDropThreshold ? static_cast((queue - kDropThreshold) / 60) : 0u; const std::size_t inFlightHint = queue > 0 ? 1u : 0u; const uint32_t bias = useDynamicBias ? TuningManager::testing_computeEmbedScaleBias( queue, droppedDelta, queue, inFlightHint) : 0u; const uint32_t target = kBaseTarget + bias; targetAccumulator += target; const uint32_t limiterSlots = std::max(1u, limiter.effectiveLimit()); const uint32_t workers = std::min(target, limiterSlots); const std::size_t demand = queue + arrivals; const std::size_t capacity = static_cast(workers) * kPerWorkerRate; const std::size_t processed = std::min(demand, capacity); totalProcessed += processed; queue = demand - processed; peakQueue = std::max(peakQueue, queue); // Drive gradient limiter using queue-correlated RTTs. const uint32_t simulatedRttMs = static_cast(8 + std::min(120, queue / 35)); const uint32_t inflightForLimiter = std::max(1u, workers); uint32_t acquired = 0; for (uint32_t i = 0; i < inflightForLimiter; ++i) { if (limiter.tryAcquire()) { ++acquired; } } for (uint32_t i = 0; i < acquired; ++i) { limiter.onJobComplete(std::chrono::milliseconds(simulatedRttMs), true); } } SimResult out; out.peakQueue = peakQueue; out.finalQueue = queue; out.totalProcessed = totalProcessed; out.avgTarget = static_cast(targetAccumulator) / static_cast(kTicks); return out; }; const SimResult baseline = runSim(false); const SimResult dynamic = runSim(true); INFO("baseline peak=" << baseline.peakQueue << " final=" << baseline.finalQueue << " processed=" << baseline.totalProcessed << " avgTarget=" << baseline.avgTarget); INFO("dynamic peak=" << dynamic.peakQueue << " final=" << dynamic.finalQueue << " processed=" << dynamic.totalProcessed << " avgTarget=" << dynamic.avgTarget); // Dynamic bias should improve drain behavior under the same gradient limiter. CHECK(dynamic.totalProcessed >= baseline.totalProcessed); CHECK(dynamic.peakQueue <= baseline.peakQueue); CHECK(dynamic.finalQueue <= baseline.finalQueue); CHECK(dynamic.avgTarget >= baseline.avgTarget); } // ============================================================================= // Group F: End-to-end idle zero-targets via DynamicCap (Bug 1 + Bug 4 combined) // ============================================================================= // When the tuning loop writes DynamicCap(0) to all stages (idle path), the // effective per-stage concurrency must be exactly zero. This tests the full // chain: TuningManager writes caps → TuneAdvisor allocator respects them. // // TDD Red Phase: FAILS because DynamicCap(0) is the unset sentinel (Bug 1) // AND the allocator has hardcoded minimums (Bug 4). TEST_CASE("Idle DynamicCap(0) on all stages yields zero effective concurrency (E2E)", "[daemon][tune][reconciliation][idle][e2e][catch2]") { resetPostIngestOverrides(); setAllPostIngestStagesActive(); HwGuard hwGuard(16); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); TuneAdvisor::setPostIngestTotalConcurrent(12); // Simulate what TuningManager::tick_once does when idle: write DynamicCap(0) to all stages TuneAdvisor::beginDynamicCapWrite(); TuneAdvisor::setPostExtractionConcurrentDynamicCap(0); TuneAdvisor::setPostKgConcurrentDynamicCap(0); TuneAdvisor::setPostSymbolConcurrentDynamicCap(0); TuneAdvisor::setPostEntityConcurrentDynamicCap(0); TuneAdvisor::setPostTitleConcurrentDynamicCap(0); TuneAdvisor::setPostEmbedConcurrentDynamicCap(0); TuneAdvisor::endDynamicCapWrite(); // Read back via the public per-stage accessors (includes DynamicCaps) const uint32_t total = TuneAdvisor::postExtractionConcurrent() + TuneAdvisor::postKgConcurrent() + TuneAdvisor::postSymbolConcurrent() + TuneAdvisor::postEntityConcurrent() + TuneAdvisor::postTitleConcurrent() + TuneAdvisor::postEmbedConcurrent(); // BUG: DynamicCap(0) = unset sentinel → caps ignored → total >= 6 // FIXED: total == 0 CHECK(total == 0); } TEST_CASE("Clearing DynamicCaps after idle restores non-zero defaults (E2E)", "[daemon][tune][reconciliation][idle][e2e][recovery][catch2]") { resetPostIngestOverrides(); setAllPostIngestStagesActive(); HwGuard hwGuard(16); ProfileGuard profileGuard(TuneAdvisor::Profile::Balanced); TuneAdvisor::setPostIngestTotalConcurrent(12); // Capture defaults const auto defaultExtract = TuneAdvisor::postExtractionDefaultConcurrent(); const auto defaultEmbed = TuneAdvisor::postEmbedDefaultConcurrent(); REQUIRE(defaultExtract >= 1); REQUIRE(defaultEmbed >= 1); // Go idle: cap everything to 0 TuneAdvisor::beginDynamicCapWrite(); TuneAdvisor::setPostExtractionConcurrentDynamicCap(0); TuneAdvisor::setPostKgConcurrentDynamicCap(0); TuneAdvisor::setPostSymbolConcurrentDynamicCap(0); TuneAdvisor::setPostEntityConcurrentDynamicCap(0); TuneAdvisor::setPostTitleConcurrentDynamicCap(0); TuneAdvisor::setPostEmbedConcurrentDynamicCap(0); TuneAdvisor::endDynamicCapWrite(); // Verify idle state (after Bug 1+4 fix) CHECK(TuneAdvisor::postExtractionConcurrent() == 0); CHECK(TuneAdvisor::postEmbedConcurrent() == 0); // Wake up: clear caps (set to UINT32_MAX sentinel — after Bug 1 fix) // In current code, clearing is done by writing 0. After fix, clearing // is done by writing UINT32_MAX. TuneAdvisor::beginDynamicCapWrite(); TuneAdvisor::setPostExtractionConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostKgConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostSymbolConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostEntityConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostTitleConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::setPostEmbedConcurrentDynamicCap(UINT32_MAX); TuneAdvisor::endDynamicCapWrite(); // Defaults should be fully restored CHECK(TuneAdvisor::postExtractionConcurrent() == defaultExtract); CHECK(TuneAdvisor::postEmbedConcurrent() == defaultEmbed); }