lib/model/unittest/CResourceMonitorTest.cc (471 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the following additional limitation. Functionality enabled by the
* files subject to the Elastic License 2.0 may only be used in production when
* invoked by an Elasticsearch process with a license key installed that permits
* use of machine learning features. You may not use this file except in
* compliance with the Elastic License 2.0 and the foregoing additional
* limitation.
*/
#include <core/CLogger.h>
#include <core/CMemoryDef.h>
#include <core/CWordDictionary.h>
#include <core/Constants.h>
#include <model/CAnomalyDetector.h>
#include <model/CAnomalyDetectorModelConfig.h>
#include <model/CHierarchicalResults.h>
#include <model/CLimits.h>
#include <model/CResourceMonitor.h>
#include <model/CTokenListDataCategorizer.h>
#include <boost/test/unit_test.hpp>
#include <string>
BOOST_AUTO_TEST_SUITE(CResourceMonitorTest)
using namespace ml;
using namespace model;
class CTestFixture {
public:
CTestFixture() {}
void reportCallback(const CResourceMonitor::SModelSizeStats& modelSizeStats) {
m_ReportedModelSizeStats = modelSizeStats;
}
void addTestData(core_t::TTime& firstTime,
const core_t::TTime bucketLength,
const std::size_t buckets,
const std::size_t newPeoplePerBucket,
std::size_t& startOffset,
CAnomalyDetector& detector,
CResourceMonitor& monitor) {
std::string numberValue("100");
core_t::TTime bucketStart = firstTime;
CHierarchicalResults results;
std::string pervasive("IShouldNotBeRemoved");
for (core_t::TTime time = firstTime;
time < static_cast<core_t::TTime>(firstTime + bucketLength * buckets);
time += (bucketLength / std::max(std::size_t(1), newPeoplePerBucket))) {
bool newBucket = false;
for (; bucketStart + bucketLength <= time; bucketStart += bucketLength) {
detector.buildResults(bucketStart, bucketStart + bucketLength, results);
monitor.pruneIfRequired(bucketStart);
monitor.updateMoments(monitor.totalMemory(), bucketStart, bucketLength);
newBucket = true;
}
if (newBucket) {
CAnomalyDetector::TStrCPtrVec fieldValues;
fieldValues.push_back(&pervasive);
fieldValues.push_back(&numberValue);
detector.addRecord(time, fieldValues);
}
if (newPeoplePerBucket > 0) {
CAnomalyDetector::TStrCPtrVec fieldValues;
std::ostringstream ss1;
ss1 << "person" << startOffset++;
std::string value(ss1.str());
fieldValues.push_back(&value);
fieldValues.push_back(&numberValue);
detector.addRecord(time, fieldValues);
}
}
firstTime = bucketStart;
}
protected:
CResourceMonitor::SModelSizeStats m_ReportedModelSizeStats;
};
BOOST_FIXTURE_TEST_CASE(testMonitor, CTestFixture) {
static const std::string EMPTY_STRING;
static const core_t::TTime FIRST_TIME{358556400};
static const core_t::TTime BUCKET_LENGTH{3600};
CAnomalyDetectorModelConfig modelConfig =
CAnomalyDetectorModelConfig::defaultConfig(BUCKET_LENGTH);
CLimits limits;
model::CTokenListDataCategorizer<> categorizer(limits, nullptr, 0.7, "whatever");
CSearchKey key1(1, // detectorIndex
function_t::E_IndividualMetric, false, model_t::E_XF_None,
"value", "colour");
CAnomalyDetector detector1(limits, modelConfig, EMPTY_STRING, FIRST_TIME,
modelConfig.factory(key1));
CSearchKey key2(2, // detectorIndex
function_t::E_IndividualMetric, false, model_t::E_XF_None,
"value", "colour");
CAnomalyDetector detector2(limits, modelConfig, EMPTY_STRING, FIRST_TIME,
modelConfig.factory(key2));
std::size_t mem = core::memory::dynamicSize(&categorizer) +
core::memory::dynamicSize(&detector1) +
core::memory::dynamicSize(&detector2);
{
// Test default constructor
CResourceMonitor mon;
BOOST_TEST_REQUIRE(mon.m_ByteLimitHigh > 0);
BOOST_REQUIRE_EQUAL((49 * mon.m_ByteLimitHigh) / 50, mon.m_ByteLimitLow);
BOOST_TEST_REQUIRE(mon.m_ByteLimitHigh > mon.m_ByteLimitLow);
BOOST_TEST_REQUIRE(mon.m_AllowAllocations);
LOG_DEBUG(<< "Resource limit is: " << mon.m_ByteLimitHigh);
if (sizeof(std::size_t) == 8) {
// 64-bit platform
BOOST_REQUIRE_EQUAL(std::size_t(4 * core::constants::BYTES_IN_GIGABYTES / 2),
mon.m_ByteLimitHigh);
} else {
// Unexpected platform
BOOST_TEST_REQUIRE(false);
}
}
{
// Test foreground persistence constructor
CResourceMonitor mon(true);
BOOST_TEST_REQUIRE(mon.m_ByteLimitHigh > 0);
BOOST_REQUIRE_EQUAL((49 * mon.m_ByteLimitHigh) / 50, mon.m_ByteLimitLow);
BOOST_TEST_REQUIRE(mon.m_ByteLimitHigh > mon.m_ByteLimitLow);
BOOST_TEST_REQUIRE(mon.m_AllowAllocations);
LOG_DEBUG(<< "Resource limit is: " << mon.m_ByteLimitHigh);
if (sizeof(std::size_t) == 8) {
// 64-bit platform
BOOST_REQUIRE_EQUAL(std::size_t(4 * core::constants::BYTES_IN_GIGABYTES),
mon.m_ByteLimitHigh);
} else {
// Unexpected platform
BOOST_TEST_REQUIRE(false);
}
}
{
// Test size constructor
CResourceMonitor mon;
mon.memoryLimit(543);
BOOST_REQUIRE_EQUAL(std::size_t(569376768 / 2), mon.m_ByteLimitHigh);
BOOST_REQUIRE_EQUAL(std::size_t((49 * 569376768ull / 2) / 50), mon.m_ByteLimitLow);
BOOST_TEST_REQUIRE(mon.m_AllowAllocations);
// Test memoryLimit
mon.memoryLimit(987);
BOOST_REQUIRE_EQUAL(std::size_t(1034944512ull / 2), mon.m_ByteLimitHigh);
BOOST_REQUIRE_EQUAL(std::size_t((49 * 1034944512ull / 2) / 50), mon.m_ByteLimitLow);
}
{
// Test adding and removing a CAnomalyDetector
CResourceMonitor mon;
BOOST_REQUIRE_EQUAL(0, mon.m_Resources.size());
BOOST_REQUIRE_EQUAL(0, mon.m_MonitoredResourceCurrentMemory);
BOOST_REQUIRE_EQUAL(0, mon.m_PreviousTotal);
mon.registerComponent(categorizer);
BOOST_REQUIRE_EQUAL(1, mon.m_Resources.size());
mon.registerComponent(detector1);
BOOST_REQUIRE_EQUAL(2, mon.m_Resources.size());
mon.registerComponent(detector2);
BOOST_REQUIRE_EQUAL(3, mon.m_Resources.size());
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
mon.sendMemoryUsageReportIfSignificantlyChanged(0, 1);
BOOST_REQUIRE_EQUAL(mem, mon.totalMemory());
BOOST_REQUIRE_EQUAL(mem, mon.m_PreviousTotal);
mon.unRegisterComponent(detector2);
BOOST_REQUIRE_EQUAL(2, mon.m_Resources.size());
mon.unRegisterComponent(detector1);
BOOST_REQUIRE_EQUAL(1, mon.m_Resources.size());
mon.unRegisterComponent(categorizer);
BOOST_REQUIRE_EQUAL(0, mon.m_Resources.size());
}
{
// Check that High limit can be breached and then gone back
CResourceMonitor mon(false, 1.0);
BOOST_TEST_REQUIRE(mem > 5); // This SHOULD be OK
// Let's go above the low but below the high limit
mon.m_ByteLimitHigh = mem + 1;
mon.m_ByteLimitLow = mem - 1;
mon.registerComponent(categorizer);
mon.registerComponent(detector1);
mon.registerComponent(detector2);
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
BOOST_REQUIRE_EQUAL(mem, mon.totalMemory());
BOOST_REQUIRE_EQUAL(true, mon.areAllocationsAllowed());
BOOST_TEST_REQUIRE(mon.allocationLimit() > 0);
// Let's go above the high and low limit
mon.m_ByteLimitHigh = mem - 1;
mon.m_ByteLimitLow = mem - 2;
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
BOOST_REQUIRE_EQUAL(mem, mon.totalMemory());
BOOST_REQUIRE_EQUAL(false, mon.areAllocationsAllowed());
BOOST_REQUIRE_EQUAL(0, mon.allocationLimit());
// Let's go above the low limit but above the high limit
mon.m_ByteLimitHigh = mem + 1;
mon.m_ByteLimitLow = mem - 1;
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
BOOST_REQUIRE_EQUAL(mem, mon.totalMemory());
BOOST_REQUIRE_EQUAL(false, mon.areAllocationsAllowed());
// Let's go below the high and low limit
mon.m_ByteLimitHigh = mem + 2;
mon.m_ByteLimitLow = mem + 1;
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
BOOST_REQUIRE_EQUAL(mem, mon.totalMemory());
BOOST_REQUIRE_EQUAL(true, mon.areAllocationsAllowed());
}
{
// Test the report callback
CResourceMonitor mon;
mon.registerComponent(categorizer);
mon.registerComponent(detector1);
mon.registerComponent(detector2);
mon.memoryUsageReporter(std::bind_front(&CTestFixture::reportCallback, this));
m_ReportedModelSizeStats.s_Usage = 0;
BOOST_REQUIRE_EQUAL(0, m_ReportedModelSizeStats.s_Usage);
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
mon.sendMemoryUsageReportIfSignificantlyChanged(0, 1);
BOOST_REQUIRE_EQUAL(mem, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusOk, m_ReportedModelSizeStats.s_MemoryStatus);
}
{
// As above but refreshing all resources in one call
CResourceMonitor mon;
mon.registerComponent(categorizer);
mon.registerComponent(detector1);
mon.registerComponent(detector2);
mon.memoryUsageReporter(std::bind_front(&CTestFixture::reportCallback, this));
m_ReportedModelSizeStats.s_Usage = 0;
BOOST_REQUIRE_EQUAL(0, m_ReportedModelSizeStats.s_Usage);
mon.forceRefreshAll();
mon.sendMemoryUsageReportIfSignificantlyChanged(0, 1);
BOOST_REQUIRE_EQUAL(mem, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusOk, m_ReportedModelSizeStats.s_MemoryStatus);
}
{
// Test the report callback for allocation failures
CResourceMonitor mon;
mon.registerComponent(categorizer);
mon.registerComponent(detector1);
mon.registerComponent(detector2);
mon.memoryUsageReporter(std::bind_front(&CTestFixture::reportCallback, this));
m_ReportedModelSizeStats.s_AllocationFailures = 0;
BOOST_REQUIRE_EQUAL(0, m_ReportedModelSizeStats.s_AllocationFailures);
// Set a soft-limit degraded status
mon.startPruning();
// This refresh should trigger a report
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
mon.sendMemoryUsageReportIfSignificantlyChanged(0, 1);
BOOST_REQUIRE_EQUAL(0, m_ReportedModelSizeStats.s_AllocationFailures);
BOOST_REQUIRE_EQUAL(mem, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusSoftLimit,
m_ReportedModelSizeStats.s_MemoryStatus);
// Set some canary values
m_ReportedModelSizeStats.s_AllocationFailures = 12345;
m_ReportedModelSizeStats.s_ByFields = 54321;
m_ReportedModelSizeStats.s_OverFields = 23456;
m_ReportedModelSizeStats.s_PartitionFields = 65432;
m_ReportedModelSizeStats.s_Usage = 1357924;
// This should not trigger a report
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
mon.sendMemoryUsageReportIfSignificantlyChanged(0, 1);
BOOST_REQUIRE_EQUAL(12345, m_ReportedModelSizeStats.s_AllocationFailures);
BOOST_REQUIRE_EQUAL(54321, m_ReportedModelSizeStats.s_ByFields);
BOOST_REQUIRE_EQUAL(23456, m_ReportedModelSizeStats.s_OverFields);
BOOST_REQUIRE_EQUAL(65432, m_ReportedModelSizeStats.s_PartitionFields);
BOOST_REQUIRE_EQUAL(1357924, m_ReportedModelSizeStats.s_Usage);
// Add some memory failures, which should be reported
mon.acceptAllocationFailureResult(14400000);
mon.acceptAllocationFailureResult(14400000);
mon.acceptAllocationFailureResult(14401000);
mon.acceptAllocationFailureResult(14402000);
BOOST_REQUIRE_EQUAL(3, mon.m_AllocationFailuresCount);
BOOST_REQUIRE_EQUAL(core_t::TTime(0), mon.m_LastAllocationFailureReport);
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
mon.sendMemoryUsageReportIfSignificantlyChanged(0, 1);
BOOST_REQUIRE_EQUAL(3, m_ReportedModelSizeStats.s_AllocationFailures);
BOOST_REQUIRE_EQUAL(mem, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(core_t::TTime(14402000), mon.m_LastAllocationFailureReport);
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusHardLimit,
m_ReportedModelSizeStats.s_MemoryStatus);
// Set some canary values
m_ReportedModelSizeStats.s_AllocationFailures = 12345;
m_ReportedModelSizeStats.s_ByFields = 54321;
m_ReportedModelSizeStats.s_OverFields = 23456;
m_ReportedModelSizeStats.s_PartitionFields = 65432;
m_ReportedModelSizeStats.s_Usage = 1357924;
// As nothing has changed, nothing should be reported
mon.refresh(categorizer);
mon.refresh(detector1);
mon.refresh(detector2);
mon.sendMemoryUsageReportIfSignificantlyChanged(0, 1);
BOOST_REQUIRE_EQUAL(12345, m_ReportedModelSizeStats.s_AllocationFailures);
BOOST_REQUIRE_EQUAL(54321, m_ReportedModelSizeStats.s_ByFields);
BOOST_REQUIRE_EQUAL(23456, m_ReportedModelSizeStats.s_OverFields);
BOOST_REQUIRE_EQUAL(65432, m_ReportedModelSizeStats.s_PartitionFields);
BOOST_REQUIRE_EQUAL(1357924, m_ReportedModelSizeStats.s_Usage);
}
{
// Test the need to report usage based on a change in levels, up and down
CResourceMonitor mon;
mon.memoryUsageReporter(std::bind_front(&CTestFixture::reportCallback, this));
BOOST_TEST_REQUIRE(!mon.needToSendReport(
model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
std::size_t origTotalMemory = mon.totalMemory();
// Go up to 10 bytes, triggering a need
mon.m_MonitoredResourceCurrentMemory = 10;
BOOST_TEST_REQUIRE(
mon.needToSendReport(model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
mon.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(origTotalMemory + 10, m_ReportedModelSizeStats.s_Usage);
// Nothing new added, so no report
BOOST_TEST_REQUIRE(!mon.needToSendReport(
model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
// 10% increase should trigger a need
mon.m_MonitoredResourceCurrentMemory += 1 + (origTotalMemory + 9) / 10;
BOOST_TEST_REQUIRE(
mon.needToSendReport(model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
mon.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(origTotalMemory + 11 + (origTotalMemory + 9) / 10,
m_ReportedModelSizeStats.s_Usage);
// Huge increase should trigger a need
mon.m_MonitoredResourceCurrentMemory = 1000;
BOOST_TEST_REQUIRE(
mon.needToSendReport(model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
mon.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(origTotalMemory + 1000, m_ReportedModelSizeStats.s_Usage);
// 0.1% increase should not trigger a need
mon.m_MonitoredResourceCurrentMemory += 1 + (origTotalMemory + 999) / 1000;
BOOST_TEST_REQUIRE(!mon.needToSendReport(
model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
// A decrease should trigger a need
mon.m_MonitoredResourceCurrentMemory = 900;
BOOST_TEST_REQUIRE(
mon.needToSendReport(model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
mon.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(origTotalMemory + 900, m_ReportedModelSizeStats.s_Usage);
// A tiny decrease should not trigger a need
mon.m_MonitoredResourceCurrentMemory = 899;
BOOST_TEST_REQUIRE(!mon.needToSendReport(
model_t::E_AssignmentBasisCurrentModelBytes, 0, 1));
}
}
BOOST_FIXTURE_TEST_CASE(testPruning, CTestFixture) {
static const std::string EMPTY_STRING;
static const core_t::TTime FIRST_TIME{358556400};
static const core_t::TTime BUCKET_LENGTH{3600};
CAnomalyDetectorModelConfig modelConfig =
CAnomalyDetectorModelConfig::defaultConfig(BUCKET_LENGTH);
CLimits limits(false, 1.0);
CSearchKey key(1, // detectorIndex
function_t::E_IndividualMetric, false, model_t::E_XF_None,
"value", "colour");
CResourceMonitor& monitor = limits.resourceMonitor();
monitor.memoryLimit(140);
CAnomalyDetector detector(limits, modelConfig, EMPTY_STRING, FIRST_TIME,
modelConfig.factory(key));
core_t::TTime bucket = FIRST_TIME;
std::size_t startOffset = 10;
// Add a couple of buckets of data
this->addTestData(bucket, BUCKET_LENGTH, 2, 5, startOffset, detector, monitor);
BOOST_REQUIRE_EQUAL(false, monitor.pruneIfRequired(bucket));
BOOST_REQUIRE_EQUAL(false, monitor.m_HasPruningStarted);
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusOk, monitor.m_MemoryStatus);
// Memory should not be stable, as we've only seen 2 buckets
BOOST_REQUIRE_EQUAL(false, monitor.isMemoryStable(BUCKET_LENGTH));
LOG_DEBUG(<< "Saturating the pruner");
// Add enough data to saturate the pruner
this->addTestData(bucket, BUCKET_LENGTH, 1100, 3, startOffset, detector, monitor);
LOG_DEBUG(<< "Window is now: " << monitor.m_PruneWindow);
BOOST_REQUIRE_EQUAL(true, monitor.m_HasPruningStarted);
BOOST_TEST_REQUIRE(monitor.m_PruneWindow < std::size_t(1000));
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusSoftLimit, monitor.m_MemoryStatus);
// Memory should be stable now, after 1100 more buckets,
// and with the pruner running
BOOST_REQUIRE_EQUAL(true, monitor.isMemoryStable(BUCKET_LENGTH));
BOOST_REQUIRE_EQUAL(true, monitor.m_AllowAllocations);
LOG_DEBUG(<< "Allowing pruner to relax");
// Add no new people and see that the window relaxes away from the minimum window
this->addTestData(bucket, BUCKET_LENGTH, 100, 0, startOffset, detector, monitor);
LOG_DEBUG(<< "Window is now: " << monitor.m_PruneWindow);
std::size_t level = monitor.m_PruneWindow;
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusSoftLimit, monitor.m_MemoryStatus);
BOOST_REQUIRE_EQUAL(true, monitor.m_AllowAllocations);
BOOST_TEST_REQUIRE(monitor.totalMemory() < monitor.m_PruneThreshold);
LOG_DEBUG(<< "Testing fine-grained control");
// Check that the window keeps growing now
this->addTestData(bucket, BUCKET_LENGTH, 50, 0, startOffset, detector, monitor);
BOOST_TEST_REQUIRE(monitor.m_PruneWindow > level);
level = monitor.m_PruneWindow;
this->addTestData(bucket, BUCKET_LENGTH, 50, 0, startOffset, detector, monitor);
BOOST_TEST_REQUIRE(monitor.m_PruneWindow > level);
level = monitor.m_PruneWindow;
// Check that adding a bunch of new data shrinks the window again
this->addTestData(bucket, BUCKET_LENGTH, 200, 10, startOffset, detector, monitor);
BOOST_TEST_REQUIRE(monitor.m_PruneWindow < level);
level = monitor.m_PruneWindow;
// And finally that it grows again
this->addTestData(bucket, BUCKET_LENGTH, 700, 0, startOffset, detector, monitor);
BOOST_TEST_REQUIRE(monitor.m_PruneWindow > level);
// If it grows enough we will stop pruning and revert to memory status OK,
// and 8000 completely empty buckets (even without the all pervasive person)
// is the quickest way to achieve this.
bucket += BUCKET_LENGTH * 8000;
this->addTestData(bucket, BUCKET_LENGTH, 2, 0, startOffset, detector, monitor);
LOG_DEBUG(<< "Window is now: " << monitor.m_PruneWindow);
BOOST_REQUIRE_EQUAL(false, monitor.m_HasPruningStarted);
BOOST_REQUIRE_EQUAL(monitor.m_PruneWindowMaximum, monitor.m_PruneWindow);
BOOST_REQUIRE_EQUAL(model_t::E_MemoryStatusOk, monitor.m_MemoryStatus);
}
BOOST_FIXTURE_TEST_CASE(testExtraMemory, CTestFixture) {
static const std::string EMPTY_STRING;
static const core_t::TTime FIRST_TIME{358556400};
static const core_t::TTime BUCKET_LENGTH{3600};
CAnomalyDetectorModelConfig modelConfig =
CAnomalyDetectorModelConfig::defaultConfig(BUCKET_LENGTH);
CLimits limits;
CSearchKey key(1, // detectorIndex
function_t::E_IndividualMetric, false, model_t::E_XF_None,
"value", "colour");
CResourceMonitor& monitor = limits.resourceMonitor();
// set the limit to 1 MB
monitor.memoryLimit(1);
CAnomalyDetector detector(limits, modelConfig, EMPTY_STRING, FIRST_TIME,
modelConfig.factory(key));
monitor.forceRefresh(detector);
std::size_t allocationLimit = monitor.allocationLimit();
monitor.addExtraMemory(100);
monitor.addExtraMemory(100);
BOOST_TEST_REQUIRE(monitor.areAllocationsAllowed());
BOOST_REQUIRE_EQUAL(allocationLimit - 200, monitor.allocationLimit());
monitor.clearExtraMemory();
BOOST_TEST_REQUIRE(monitor.areAllocationsAllowed());
BOOST_REQUIRE_EQUAL(allocationLimit, monitor.allocationLimit());
// Push over the limit by adding 1MB
monitor.addExtraMemory(core::constants::BYTES_IN_MEGABYTES);
BOOST_TEST_REQUIRE(monitor.areAllocationsAllowed() == false);
BOOST_REQUIRE_EQUAL(0, monitor.allocationLimit());
monitor.clearExtraMemory();
BOOST_TEST_REQUIRE(monitor.areAllocationsAllowed());
BOOST_REQUIRE_EQUAL(allocationLimit, monitor.allocationLimit());
}
BOOST_FIXTURE_TEST_CASE(testPeakUsage, CTestFixture) {
// Clear the counter so that other test cases do not interfere.
core::CProgramCounters::counter(counter_t::E_TSADPeakMemoryUsage) = 0;
CLimits limits;
CResourceMonitor& monitor = limits.resourceMonitor();
monitor.memoryUsageReporter(std::bind_front(&CTestFixture::reportCallback, this));
std::size_t baseTotalMemory = monitor.totalMemory();
monitor.updateMoments(monitor.totalMemory(), 0, 1);
monitor.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(baseTotalMemory, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(baseTotalMemory, m_ReportedModelSizeStats.s_PeakUsage);
monitor.addExtraMemory(100);
monitor.updateMoments(monitor.totalMemory(), 0, 1);
monitor.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(baseTotalMemory + 100, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(baseTotalMemory + 100, m_ReportedModelSizeStats.s_PeakUsage);
monitor.addExtraMemory(-50);
monitor.updateMoments(monitor.totalMemory(), 0, 1);
monitor.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(baseTotalMemory + 50, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(baseTotalMemory + 100, m_ReportedModelSizeStats.s_PeakUsage);
monitor.addExtraMemory(100);
monitor.updateMoments(monitor.totalMemory(), 0, 1);
monitor.sendMemoryUsageReport(0, 1);
BOOST_REQUIRE_EQUAL(baseTotalMemory + 150, m_ReportedModelSizeStats.s_Usage);
BOOST_REQUIRE_EQUAL(baseTotalMemory + 150, m_ReportedModelSizeStats.s_PeakUsage);
}
BOOST_FIXTURE_TEST_CASE(testUpdateMoments, CTestFixture) {
static const core_t::TTime FIRST_TIME{358556400};
static const core_t::TTime BUCKET_LENGTH{3600};
// First a realistic case
{
CLimits limits;
CResourceMonitor& monitor = limits.resourceMonitor();
core_t::TTime time{FIRST_TIME};
std::size_t totalMemory{1000000};
// For the first 19 buckets memory is not stable due to the bucket count alone
for (std::size_t count = 0; count < 19; ++count) {
monitor.updateMoments(totalMemory, time, BUCKET_LENGTH);
BOOST_REQUIRE_EQUAL(FIRST_TIME, monitor.m_FirstMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(time, monitor.m_LastMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(false, monitor.isMemoryStable(BUCKET_LENGTH));
time += BUCKET_LENGTH;
totalMemory += 200000;
}
// At bucket 20 the coefficient of variation comes into play - initially it's
// too high, as we added 200000 bytes to the memory usage in every one of the
// first 19 buckets
for (std::size_t count = 20; count < 45; ++count) {
monitor.updateMoments(totalMemory, time, BUCKET_LENGTH);
BOOST_REQUIRE_EQUAL(FIRST_TIME, monitor.m_FirstMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(time, monitor.m_LastMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(false, monitor.isMemoryStable(BUCKET_LENGTH));
time += BUCKET_LENGTH;
}
// After several buckets of flat memory use memory should be reported as
// stable, and should continue to be reported as stable even when there
// are small fluctuations
for (std::size_t count = 46; count < 100; ++count) {
monitor.updateMoments(totalMemory, time, BUCKET_LENGTH);
BOOST_REQUIRE_EQUAL(FIRST_TIME, monitor.m_FirstMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(time, monitor.m_LastMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(true, monitor.isMemoryStable(BUCKET_LENGTH));
time += BUCKET_LENGTH;
totalMemory += (count % 5 - 2) * 1000;
}
}
// Unexpected edge cases - mean and variance both always zero
{
CLimits limits;
CResourceMonitor& monitor = limits.resourceMonitor();
core_t::TTime time{FIRST_TIME};
// Asking about stability before adding any measurements is wrong but
// should not cause a crash
BOOST_REQUIRE_EQUAL(false, monitor.isMemoryStable(BUCKET_LENGTH));
// For the first 19 buckets memory is not stable due to the bucket count alone
for (std::size_t count = 0; count < 19; ++count) {
monitor.updateMoments(0, time, BUCKET_LENGTH);
BOOST_REQUIRE_EQUAL(FIRST_TIME, monitor.m_FirstMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(time, monitor.m_LastMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(false, monitor.isMemoryStable(BUCKET_LENGTH));
time += BUCKET_LENGTH;
}
// At bucket 20 the coefficient of variation comes into play, and by its
// textbook definition it's 0/0. However, the rearrangement of the
// formula should avoid NaNs.
monitor.updateMoments(0, time, BUCKET_LENGTH);
BOOST_REQUIRE_EQUAL(FIRST_TIME, monitor.m_FirstMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(time, monitor.m_LastMomentsUpdateTime);
BOOST_REQUIRE_EQUAL(true, monitor.isMemoryStable(BUCKET_LENGTH));
}
}
BOOST_AUTO_TEST_SUITE_END()