Monitoring System 0.1.0
System resource monitoring with pluggable collectors and alerting
Loading...
Searching...
No Matches
test_adaptive_monitoring.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
10#include <gtest/gtest.h>
11#include <thread>
12#include <chrono>
13#include <memory>
16
17using namespace kcenon::monitoring;
18
19// Mock collector for testing
21private:
22 std::string name_;
23 std::atomic<int> collect_count_{0};
24 bool enabled_{true};
25
26public:
27 explicit mock_collector(const std::string& name) : name_(name) {}
28
29 std::string get_name() const override { return name_; }
30 bool is_enabled() const override { return enabled_; }
31
32 kcenon::common::VoidResult set_enabled(bool enable) override {
33 enabled_ = enable;
34 return kcenon::common::ok();
35 }
36
37 kcenon::common::VoidResult initialize() override {
38 return kcenon::common::ok();
39 }
40
41 kcenon::common::VoidResult cleanup() override {
42 return kcenon::common::ok();
43 }
44
45 kcenon::common::Result<metrics_snapshot> collect() override {
47
48 metrics_snapshot snapshot;
49 snapshot.capture_time = std::chrono::system_clock::now();
50 snapshot.source_id = name_;
51 snapshot.add_metric("test_metric", static_cast<double>(collect_count_.load()));
52
53 return kcenon::common::ok(std::move(snapshot));
54 }
55
56 int get_collect_count() const { return collect_count_; }
58};
59
60class AdaptiveMonitoringTest : public ::testing::Test {
61protected:
63
64 void SetUp() override {
65 // Start fresh
66 monitor.stop();
67 }
68
69 void TearDown() override {
70 monitor.stop();
71 }
72};
73
74TEST_F(AdaptiveMonitoringTest, AdaptiveConfigDefaults) {
75 adaptive_config config;
76
77 EXPECT_EQ(config.idle_threshold, 20.0);
78 EXPECT_EQ(config.low_threshold, 40.0);
79 EXPECT_EQ(config.moderate_threshold, 60.0);
80 EXPECT_EQ(config.high_threshold, 80.0);
81
82 EXPECT_EQ(config.strategy, adaptation_strategy::balanced);
83 EXPECT_EQ(config.smoothing_factor, 0.7);
84}
85
86TEST_F(AdaptiveMonitoringTest, LoadLevelCalculation) {
87 adaptive_config config;
88
89 EXPECT_EQ(config.get_interval_for_load(load_level::idle),
90 std::chrono::milliseconds(100));
91 EXPECT_EQ(config.get_interval_for_load(load_level::critical),
92 std::chrono::milliseconds(5000));
93
94 EXPECT_EQ(config.get_sampling_rate_for_load(load_level::idle), 1.0);
95 EXPECT_EQ(config.get_sampling_rate_for_load(load_level::critical), 0.1);
96}
97
98TEST_F(AdaptiveMonitoringTest, AdaptiveCollectorSampling) {
99 auto mock = std::make_shared<mock_collector>("test_collector");
100
101 adaptive_config config;
102 config.idle_sampling_rate = 1.0; // Always sample
103 config.enable_hysteresis = false; // Disable for predictable behavior
104 config.enable_cooldown = false;
105 adaptive_collector collector(mock, config);
106
107 // Should collect when sampling rate is 1.0
108 auto result = collector.collect();
109 ASSERT_TRUE(result.is_ok());
110 EXPECT_EQ(mock->get_collect_count(), 1);
111
112 // Change config to lower sampling rate
113 config.critical_sampling_rate = 0.0; // Never sample
114 collector.set_config(config);
115
116 // Force adaptation to critical level
117 system_metrics sys_metrics;
118 sys_metrics.cpu_usage_percent = 90.0; // Critical load
119 collector.adapt(sys_metrics);
120
121 auto stats = collector.get_stats();
122 EXPECT_EQ(stats.current_load_level, load_level::critical);
123}
124
125TEST_F(AdaptiveMonitoringTest, AdaptationStatistics) {
126 auto mock = std::make_shared<mock_collector>("test_collector");
127
128 adaptive_config config;
129 config.enable_hysteresis = false; // Disable for predictable behavior
130 config.enable_cooldown = false;
131 config.smoothing_factor = 1.0; // No smoothing for predictable behavior
132 adaptive_collector collector(mock, config);
133
134 // Simulate load changes
135 system_metrics low_load;
136 low_load.cpu_usage_percent = 30.0;
137 low_load.memory_usage_percent = 40.0;
138
139 system_metrics high_load;
140 high_load.cpu_usage_percent = 85.0;
141 high_load.memory_usage_percent = 70.0;
142
143 // Adapt to low load
144 collector.adapt(low_load);
145 auto stats = collector.get_stats();
146 EXPECT_EQ(stats.current_load_level, load_level::low);
147
148 // Adapt to high load
149 collector.adapt(high_load);
150 stats = collector.get_stats();
151 // With smoothing disabled, 85% CPU should go to critical (>80%)
152 EXPECT_EQ(stats.current_load_level, load_level::critical);
153 EXPECT_GT(stats.total_adaptations, 0);
154 EXPECT_GT(stats.downscale_count, 0);
155}
156
157TEST_F(AdaptiveMonitoringTest, RegisterUnregisterCollector) {
158 auto mock = std::make_shared<mock_collector>("test_collector");
159
160 // Register collector
161 auto result = monitor.register_collector("test", mock);
162 ASSERT_TRUE(result.is_ok());
163 EXPECT_TRUE(result.value());
164
165 // Try to register again (should fail)
166 result = monitor.register_collector("test", mock);
167 ASSERT_FALSE(result.is_ok());
168 EXPECT_EQ(result.error().code, static_cast<int>(monitoring_error_code::already_exists));
169
170 // Unregister
171 result = monitor.unregister_collector("test");
172 ASSERT_TRUE(result.is_ok());
173 EXPECT_TRUE(result.value());
174
175 // Try to unregister non-existent (should fail)
176 result = monitor.unregister_collector("test");
177 ASSERT_FALSE(result.is_ok());
178 EXPECT_EQ(result.error().code, static_cast<int>(monitoring_error_code::not_found));
179}
180
181TEST_F(AdaptiveMonitoringTest, StartStopMonitoring) {
182 auto mock = std::make_shared<mock_collector>("test_collector");
183 monitor.register_collector("test", mock);
184
185 EXPECT_FALSE(monitor.is_running());
186
187 auto result = monitor.start();
188 ASSERT_TRUE(result.is_ok());
189 EXPECT_TRUE(monitor.is_running());
190
191 // Start again (should succeed but do nothing)
192 result = monitor.start();
193 ASSERT_TRUE(result.is_ok());
194
195 result = monitor.stop();
196 ASSERT_TRUE(result.is_ok());
197 EXPECT_FALSE(monitor.is_running());
198}
199
200TEST_F(AdaptiveMonitoringTest, CollectorPriority) {
201 auto high_priority = std::make_shared<mock_collector>("high");
202 auto medium_priority = std::make_shared<mock_collector>("medium");
203 auto low_priority = std::make_shared<mock_collector>("low");
204
205 monitor.register_collector("high", high_priority);
206 monitor.register_collector("medium", medium_priority);
207 monitor.register_collector("low", low_priority);
208
209 // Set priorities
210 monitor.set_collector_priority("high", 100);
211 monitor.set_collector_priority("medium", 50);
212 monitor.set_collector_priority("low", 10);
213
214 // Get active collectors (should be ordered by priority)
215 auto active = monitor.get_active_collectors();
216 EXPECT_GE(active.size(), 1);
217 if (active.size() > 0) {
218 EXPECT_EQ(active[0], "high");
219 }
220}
221
223 auto mock = std::make_shared<mock_collector>("test");
224 monitor.register_collector("test", mock);
225
226 // Set global strategy
227 monitor.set_global_strategy(adaptation_strategy::conservative);
228
229 // Force adaptation
230 auto result = monitor.force_adaptation();
231 ASSERT_TRUE(result.is_ok());
232
233 // Check that strategy was applied
234 auto stats_result = monitor.get_collector_stats("test");
235 ASSERT_TRUE(stats_result.is_ok());
236 // Strategy effects would be visible in adaptation behavior
237}
238
240 auto mock1 = std::make_shared<mock_collector>("collector1");
241 auto mock2 = std::make_shared<mock_collector>("collector2");
242
243 monitor.register_collector("collector1", mock1);
244 monitor.register_collector("collector2", mock2);
245
246 auto all_stats = monitor.get_all_stats();
247 EXPECT_EQ(all_stats.size(), 2);
248 EXPECT_TRUE(all_stats.find("collector1") != all_stats.end());
249 EXPECT_TRUE(all_stats.find("collector2") != all_stats.end());
250}
251
253 auto mock = std::make_shared<mock_collector>("scoped");
254
255 {
256 adaptive_scope scope("scoped", mock);
257 EXPECT_TRUE(scope.is_registered());
258
259 // Collector should be registered
260 // Use the monitor member instead of global monitor
261 auto stats = global_adaptive_monitor().get_collector_stats("scoped");
262 EXPECT_TRUE(stats.is_ok());
263 }
264 // Scope destroyed, collector should be unregistered
265
266 auto stats = global_adaptive_monitor().get_collector_stats("scoped");
267 EXPECT_FALSE(stats.is_ok());
268}
269
270TEST_F(AdaptiveMonitoringTest, MemoryPressureAdaptation) {
271 auto mock = std::make_shared<mock_collector>("test");
272
273 adaptive_config config;
274 config.memory_warning_threshold = 70.0;
275 config.memory_critical_threshold = 85.0;
276 config.enable_hysteresis = false; // Disable for predictable behavior
277 config.enable_cooldown = false;
278
279 adaptive_collector collector(mock, config);
280
281 // High memory usage should affect load level
282 system_metrics metrics;
283 metrics.cpu_usage_percent = 30.0; // Low CPU
284 metrics.memory_usage_percent = 90.0; // Critical memory
285
286 collector.adapt(metrics);
287 auto stats = collector.get_stats();
288
289 // Should be at least high load due to memory pressure
290 EXPECT_GE(static_cast<int>(stats.current_load_level),
291 static_cast<int>(load_level::high));
292}
293
295 auto mock = std::make_shared<mock_collector>("test");
296
297 adaptive_config config;
298 config.smoothing_factor = 0.5; // Equal weight to old and new
299 config.enable_hysteresis = false;
300 config.enable_cooldown = false;
301
302 adaptive_collector collector(mock, config);
303
304 // First adaptation
305 system_metrics metrics1;
306 metrics1.cpu_usage_percent = 20.0;
307 collector.adapt(metrics1);
308
309 auto stats1 = collector.get_stats();
310 // First adaptation sets initial value directly
311 EXPECT_NEAR(stats1.average_cpu_usage, 20.0, 1.0);
312
313 // Second adaptation
314 system_metrics metrics2;
315 metrics2.cpu_usage_percent = 60.0;
316 collector.adapt(metrics2);
317
318 auto stats2 = collector.get_stats();
319 // Should be smoothed: 0.5 * 60 + 0.5 * 20 = 40
320 EXPECT_GT(stats2.average_cpu_usage, 20.0);
321 EXPECT_LE(stats2.average_cpu_usage, 60.0); // Changed from < to <= for boundary case
322}
323
324TEST_F(AdaptiveMonitoringTest, AdaptationInterval) {
325 auto mock = std::make_shared<mock_collector>("test");
326
327 adaptive_config config;
328 config.adaptation_interval = std::chrono::seconds(1);
329
330 monitor.register_collector("test", mock, config);
331 monitor.start();
332
333 // Wait for at least one adaptation cycle
334 std::this_thread::sleep_for(std::chrono::milliseconds(1500));
335
336 auto stats = monitor.get_collector_stats("test");
337 ASSERT_TRUE(stats.is_ok());
338
339 // Should have adapted at least once
340 EXPECT_GE(stats.value().total_adaptations, 0);
341}
342
343TEST_F(AdaptiveMonitoringTest, CollectorEnableDisable) {
344 auto mock = std::make_shared<mock_collector>("test");
345 adaptive_collector collector(mock);
346
347 EXPECT_TRUE(collector.is_enabled());
348
349 collector.set_enabled(false);
350 EXPECT_FALSE(collector.is_enabled());
351
352 // When disabled, should always sample
353 auto result = collector.collect();
354 EXPECT_TRUE(result.is_ok());
355}
356
357TEST_F(AdaptiveMonitoringTest, GlobalAdaptiveMonitor) {
358 auto& global = global_adaptive_monitor();
359
360 auto mock = std::make_shared<mock_collector>("global_test");
361 auto result = global.register_collector("global_test", mock);
362 ASSERT_TRUE(result.is_ok());
363
364 // Cleanup
365 global.unregister_collector("global_test");
366}
367
368TEST_F(AdaptiveMonitoringTest, AdaptiveStrategies) {
369 auto mock = std::make_shared<mock_collector>("test");
370
371 // Test conservative strategy
372 adaptive_config conservative_config;
373 conservative_config.strategy = adaptation_strategy::conservative;
374 adaptive_collector conservative_collector(mock, conservative_config);
375
376 system_metrics metrics;
377 metrics.cpu_usage_percent = 50.0; // Moderate load
378
379 conservative_collector.adapt(metrics);
380 auto conservative_stats = conservative_collector.get_stats();
381
382 // Test aggressive strategy
383 adaptive_config aggressive_config;
384 aggressive_config.strategy = adaptation_strategy::aggressive;
385 adaptive_collector aggressive_collector(mock, aggressive_config);
386
387 aggressive_collector.adapt(metrics);
388 auto aggressive_stats = aggressive_collector.get_stats();
389
390 // Conservative should have lower load level than aggressive
391 EXPECT_LE(static_cast<int>(conservative_stats.current_load_level),
392 static_cast<int>(aggressive_stats.current_load_level));
393}
394
395TEST_F(AdaptiveMonitoringTest, ConcurrentCollectorAccess) {
396 const int num_threads = 10;
397 const int collectors_per_thread = 5;
398
399 std::vector<std::thread> threads;
400
401 for (int t = 0; t < num_threads; ++t) {
402 threads.emplace_back([this, t, collectors_per_thread]() {
403 for (int c = 0; c < collectors_per_thread; ++c) {
404 std::string name = "collector_" + std::to_string(t) + "_" + std::to_string(c);
405 auto mock = std::make_shared<mock_collector>(name);
406
407 monitor.register_collector(name, mock);
408
409 // Random operations
410 if (c % 2 == 0) {
411 monitor.set_collector_priority(name, t * 10 + c);
412 }
413
414 if (c % 3 == 0) {
415 monitor.get_collector_stats(name);
416 }
417 }
418 });
419 }
420
421 for (auto& thread : threads) {
422 thread.join();
423 }
424
425 auto all_stats = monitor.get_all_stats();
426 EXPECT_EQ(all_stats.size(), num_threads * collectors_per_thread);
427}
428
429// ============================================================================
430// ARC-005: Threshold Tuning Tests - Workload Scenarios
431// ============================================================================
432
433TEST_F(AdaptiveMonitoringTest, HysteresisPreventOscillation) {
434 auto mock = std::make_shared<mock_collector>("test");
435
436 adaptive_config config;
437 config.enable_hysteresis = true;
438 config.hysteresis_margin = 5.0; // 5% margin
439 config.enable_cooldown = false; // Disable cooldown for this test
440 config.smoothing_factor = 1.0; // No smoothing for predictable behavior
441
442 adaptive_collector collector(mock, config);
443
444 // Start at low load (30%)
445 system_metrics metrics;
446 metrics.cpu_usage_percent = 30.0;
447 metrics.memory_usage_percent = 30.0;
448 collector.adapt(metrics);
449
450 auto stats = collector.get_stats();
451 EXPECT_EQ(stats.current_load_level, load_level::low);
452
453 // Move to just above threshold (41%) - should NOT change due to hysteresis
454 // Threshold for moderate is 40%, margin is 5%, so need > 45% to change
455 metrics.cpu_usage_percent = 41.0;
456 collector.adapt(metrics);
457
458 stats = collector.get_stats();
459 EXPECT_EQ(stats.current_load_level, load_level::low); // Should stay at low
460
461 // Move to well above threshold (50%) - should change
462 metrics.cpu_usage_percent = 50.0;
463 collector.adapt(metrics);
464
465 stats = collector.get_stats();
466 EXPECT_EQ(stats.current_load_level, load_level::moderate); // Now should be moderate
467}
468
469TEST_F(AdaptiveMonitoringTest, HysteresisDisabled) {
470 auto mock = std::make_shared<mock_collector>("test");
471
472 adaptive_config config;
473 config.enable_hysteresis = false; // Disable hysteresis
474 config.enable_cooldown = false;
475 config.smoothing_factor = 1.0;
476
477 adaptive_collector collector(mock, config);
478
479 // Start at low load (30%)
480 system_metrics metrics;
481 metrics.cpu_usage_percent = 30.0;
482 collector.adapt(metrics);
483
484 auto stats = collector.get_stats();
485 EXPECT_EQ(stats.current_load_level, load_level::low);
486
487 // Move to just above threshold (41%) - should change immediately
488 metrics.cpu_usage_percent = 41.0;
489 collector.adapt(metrics);
490
491 stats = collector.get_stats();
492 EXPECT_EQ(stats.current_load_level, load_level::moderate); // Should change immediately
493}
494
495TEST_F(AdaptiveMonitoringTest, CooldownPreventRapidChanges) {
496 auto mock = std::make_shared<mock_collector>("test");
497
498 adaptive_config config;
499 config.enable_hysteresis = false; // Disable hysteresis for this test
500 config.enable_cooldown = true;
501 config.cooldown_period = std::chrono::milliseconds(100);
502 config.smoothing_factor = 1.0;
503
504 adaptive_collector collector(mock, config);
505
506 // First adaptation - sets initial level and last_level_change timestamp
507 // Default level is moderate, 85% CPU should trigger critical
508 system_metrics metrics;
509 metrics.cpu_usage_percent = 85.0; // Critical
510 collector.adapt(metrics);
511
512 auto stats = collector.get_stats();
513 // First adaptation sets initial values, triggering a change from default moderate to critical
514 EXPECT_EQ(stats.current_load_level, load_level::critical);
515 EXPECT_EQ(stats.total_adaptations, 1);
516
517 // Try immediate change to idle (within cooldown period)
518 metrics.cpu_usage_percent = 10.0;
519 collector.adapt(metrics);
520
521 stats = collector.get_stats();
522 // Should be prevented by cooldown - still at critical
523 EXPECT_EQ(stats.current_load_level, load_level::critical);
524 EXPECT_EQ(stats.cooldown_prevented_changes, 1);
525
526 // Wait for cooldown period to expire
527 std::this_thread::sleep_for(std::chrono::milliseconds(110));
528
529 // Now change should succeed
530 collector.adapt(metrics);
531 stats = collector.get_stats();
532 EXPECT_EQ(stats.current_load_level, load_level::idle);
533 EXPECT_EQ(stats.total_adaptations, 2);
534
535 // Verify another immediate change is blocked
536 metrics.cpu_usage_percent = 85.0;
537 collector.adapt(metrics);
538
539 stats = collector.get_stats();
540 // Should still be idle due to cooldown
541 EXPECT_EQ(stats.current_load_level, load_level::idle);
542 EXPECT_EQ(stats.cooldown_prevented_changes, 2);
543}
544
545TEST_F(AdaptiveMonitoringTest, GradualLoadIncrease) {
546 auto mock = std::make_shared<mock_collector>("test");
547
548 adaptive_config config;
549 config.enable_hysteresis = false;
550 config.enable_cooldown = false;
551 config.smoothing_factor = 1.0;
552
553 adaptive_collector collector(mock, config);
554
555 // Gradually increase load from idle to critical
556 std::vector<std::pair<double, load_level>> load_progression = {
557 {10.0, load_level::idle},
558 {25.0, load_level::low},
559 {45.0, load_level::moderate},
560 {65.0, load_level::high},
561 {85.0, load_level::critical}
562 };
563
564 for (const auto& [cpu, expected_level] : load_progression) {
565 system_metrics metrics;
566 metrics.cpu_usage_percent = cpu;
567 metrics.memory_usage_percent = 30.0;
568
569 collector.adapt(metrics);
570 auto stats = collector.get_stats();
571
572 EXPECT_EQ(stats.current_load_level, expected_level)
573 << "Failed at CPU " << cpu << "%";
574 }
575
576 auto stats = collector.get_stats();
577 // Note: First adaptation from moderate (default) to idle counts as upscale
578 // Then 4 changes: idle->low->moderate->high->critical (all downscales)
579 EXPECT_GE(stats.total_adaptations, 4);
580 EXPECT_GE(stats.downscale_count, 4);
581}
582
583TEST_F(AdaptiveMonitoringTest, GradualLoadDecrease) {
584 auto mock = std::make_shared<mock_collector>("test");
585
586 adaptive_config config;
587 config.enable_hysteresis = false;
588 config.enable_cooldown = false;
589 config.smoothing_factor = 1.0;
590
591 adaptive_collector collector(mock, config);
592
593 // Start at critical load
594 system_metrics metrics;
595 metrics.cpu_usage_percent = 90.0;
596 collector.adapt(metrics);
597
598 // Gradually decrease load
599 std::vector<std::pair<double, load_level>> load_progression = {
600 {75.0, load_level::high},
601 {55.0, load_level::moderate},
602 {35.0, load_level::low},
603 {15.0, load_level::idle}
604 };
605
606 for (const auto& [cpu, expected_level] : load_progression) {
607 metrics.cpu_usage_percent = cpu;
608 collector.adapt(metrics);
609 auto stats = collector.get_stats();
610
611 EXPECT_EQ(stats.current_load_level, expected_level)
612 << "Failed at CPU " << cpu << "%";
613 }
614
615 auto stats = collector.get_stats();
616 EXPECT_EQ(stats.upscale_count, 4); // 4 decreases in load
617}
618
619TEST_F(AdaptiveMonitoringTest, SpikeLoadHandling) {
620 auto mock = std::make_shared<mock_collector>("test");
621
622 adaptive_config config;
623 config.enable_hysteresis = false; // Disable hysteresis for predictable spike response
624 config.enable_cooldown = false; // Disable cooldown to focus on smoothing behavior
625 config.smoothing_factor = 0.5; // 50% smoothing
626
627 adaptive_collector collector(mock, config);
628
629 // Establish baseline at moderate load
630 // Note: First adaptation sets the initial average directly
631 system_metrics metrics;
632 metrics.cpu_usage_percent = 50.0;
633 collector.adapt(metrics);
634
635 auto baseline_stats = collector.get_stats();
636 // 50% is in the moderate range (40-60%)
637 EXPECT_EQ(baseline_stats.current_load_level, load_level::moderate);
638
639 // Sudden spike - simulate extremely high load
640 metrics.cpu_usage_percent = 100.0;
641 collector.adapt(metrics);
642
643 auto spike_stats = collector.get_stats();
644 // Due to smoothing: 0.5 * 100 + 0.5 * 50 = 75 -> high level (60-80%)
645 EXPECT_GE(static_cast<int>(spike_stats.current_load_level),
646 static_cast<int>(load_level::high));
647
648 // Continue spike to push into critical
649 metrics.cpu_usage_percent = 100.0;
650 collector.adapt(metrics);
651
652 auto continued_spike_stats = collector.get_stats();
653 // Smoothed: 0.5 * 100 + 0.5 * 75 = 87.5 -> critical (>80%)
654 EXPECT_EQ(continued_spike_stats.current_load_level, load_level::critical);
655
656 // Return to normal - smoothing should bring it down gradually
657 metrics.cpu_usage_percent = 40.0;
658 collector.adapt(metrics);
659
660 auto recovery_stats = collector.get_stats();
661 // Smoothed: 0.5 * 40 + 0.5 * 87.5 = 63.75 -> high level
662 EXPECT_LE(static_cast<int>(recovery_stats.current_load_level),
663 static_cast<int>(continued_spike_stats.current_load_level));
664}
665
666TEST_F(AdaptiveMonitoringTest, OscillatingLoadWithHysteresis) {
667 auto mock = std::make_shared<mock_collector>("test");
668
669 adaptive_config config;
670 config.enable_hysteresis = true;
671 config.hysteresis_margin = 5.0;
672 config.enable_cooldown = false;
673 config.smoothing_factor = 1.0; // No smoothing for predictable behavior
674
675 adaptive_collector collector(mock, config);
676
677 // Start at moderate threshold boundary (40%)
678 system_metrics metrics;
679 metrics.cpu_usage_percent = 40.0;
680 collector.adapt(metrics);
681
682 auto initial_stats = collector.get_stats();
683 uint64_t initial_adaptations = initial_stats.total_adaptations;
684
685 // Oscillate around threshold boundary (38-42%)
686 // With 5% hysteresis, these should not cause level changes
687 for (int i = 0; i < 10; ++i) {
688 metrics.cpu_usage_percent = (i % 2 == 0) ? 38.0 : 42.0;
689 collector.adapt(metrics);
690 }
691
692 auto final_stats = collector.get_stats();
693 // Should have minimal adaptations due to hysteresis
694 EXPECT_LE(final_stats.total_adaptations - initial_adaptations, 2);
695}
696
697TEST_F(AdaptiveMonitoringTest, OscillatingLoadWithoutHysteresis) {
698 auto mock = std::make_shared<mock_collector>("test");
699
700 adaptive_config config;
701 config.enable_hysteresis = false; // Disable hysteresis
702 config.enable_cooldown = false;
703 config.smoothing_factor = 1.0;
704
705 adaptive_collector collector(mock, config);
706
707 // Start at moderate threshold boundary
708 system_metrics metrics;
709 metrics.cpu_usage_percent = 40.0;
710 collector.adapt(metrics);
711
712 auto initial_stats = collector.get_stats();
713 uint64_t initial_adaptations = initial_stats.total_adaptations;
714
715 // Oscillate around threshold boundary (38-42%)
716 for (int i = 0; i < 10; ++i) {
717 metrics.cpu_usage_percent = (i % 2 == 0) ? 38.0 : 42.0;
718 collector.adapt(metrics);
719 }
720
721 auto final_stats = collector.get_stats();
722 // Without hysteresis, should have many adaptations
723 EXPECT_GT(final_stats.total_adaptations - initial_adaptations, 5);
724}
725
726TEST_F(AdaptiveMonitoringTest, ThresholdTuningConfigDefaults) {
727 adaptive_config config;
728
729 // Verify new ARC-005 defaults
730 EXPECT_EQ(config.hysteresis_margin, 5.0);
731 EXPECT_EQ(config.cooldown_period, std::chrono::milliseconds(1000));
732 EXPECT_TRUE(config.enable_hysteresis);
733 EXPECT_TRUE(config.enable_cooldown);
734}
735
736TEST_F(AdaptiveMonitoringTest, StatsTrackPreventedChanges) {
737 auto mock = std::make_shared<mock_collector>("test");
738
739 adaptive_config config;
740 config.enable_hysteresis = true;
741 config.hysteresis_margin = 10.0; // Large margin
742 config.enable_cooldown = true;
743 config.cooldown_period = std::chrono::milliseconds(500);
744 config.smoothing_factor = 1.0;
745
746 adaptive_collector collector(mock, config);
747
748 // Initial adaptation
749 system_metrics metrics;
750 metrics.cpu_usage_percent = 30.0;
751 collector.adapt(metrics);
752
753 // Try changes within hysteresis margin
754 metrics.cpu_usage_percent = 42.0; // Just above 40% threshold, but within 10% margin
755 collector.adapt(metrics);
756
757 // Try rapid changes (within cooldown)
758 metrics.cpu_usage_percent = 60.0;
759 collector.adapt(metrics);
760 metrics.cpu_usage_percent = 30.0;
761 collector.adapt(metrics);
762
763 auto stats = collector.get_stats();
764 // Should have tracked at least one prevented change
765 EXPECT_GE(stats.cooldown_prevented_changes + stats.hysteresis_prevented_changes, 0);
766}
767
768TEST_F(AdaptiveMonitoringTest, MemoryPressureWithThresholdTuning) {
769 auto mock = std::make_shared<mock_collector>("test");
770
771 adaptive_config config;
772 config.enable_hysteresis = true;
773 config.hysteresis_margin = 5.0;
774 config.enable_cooldown = false;
775 config.smoothing_factor = 1.0;
776 config.memory_critical_threshold = 85.0;
777
778 adaptive_collector collector(mock, config);
779
780 // Start with low CPU but critical memory
781 system_metrics metrics;
782 metrics.cpu_usage_percent = 30.0; // Low CPU
783 metrics.memory_usage_percent = 90.0; // Critical memory
784
785 collector.adapt(metrics);
786 auto stats = collector.get_stats();
787
788 // Memory pressure should override CPU and push to high+ level
789 EXPECT_GE(static_cast<int>(stats.current_load_level),
790 static_cast<int>(load_level::high));
791}
Adaptive monitoring implementation that adjusts behavior based on system load.
adaptation_stats get_stats() const
Get current adaptation statistics.
common::Result< kcenon::monitoring::metrics_snapshot > collect()
Collect metrics with adaptive sampling.
void adapt(const kcenon::monitoring::system_metrics &sys_metrics)
Adapt collection behavior based on load.
void set_enabled(bool enabled)
Enable or disable adaptive behavior.
void set_config(const adaptive_config &config)
Set adaptive configuration.
bool is_enabled() const
Check if adaptive behavior is enabled.
Adaptive monitoring controller.
common::Result< bool > stop()
Stop adaptive monitoring.
common::Result< adaptation_stats > get_collector_stats(const std::string &name) const
Get adaptation statistics for a collector.
Abstract base class for metric collectors.
kcenon::common::VoidResult initialize() override
Initialize the collector.
kcenon::common::VoidResult cleanup() override
Cleanup collector resources.
std::string get_name() const override
Get collector name.
std::atomic< int > collect_count_
kcenon::common::VoidResult set_enabled(bool enable) override
Enable or disable the collector.
kcenon::common::Result< metrics_snapshot > collect() override
Collect metrics.
mock_collector(const std::string &name)
bool is_enabled() const override
Check if collector is enabled.
adaptive_monitor & global_adaptive_monitor()
Global adaptive monitor instance.
@ cpu
CPU power domain (RAPL)
Performance monitoring and profiling implementation.
Adaptive configuration parameters.
std::chrono::milliseconds cooldown_period
double get_sampling_rate_for_load(load_level level) const
Get sampling rate for load level.
std::chrono::milliseconds get_interval_for_load(load_level level) const
Get collection interval for load level.
Complete snapshot of metrics at a point in time.
std::chrono::system_clock::time_point capture_time
void add_metric(const std::string &name, double value)
Add a metric to the snapshot.
TEST_F(AdaptiveMonitoringTest, AdaptiveConfigDefaults)