Files
docview/tests/test_memory_pressure.cpp
2026-09-21 13:41:40 +09:00

201 lines
9.5 KiB
C++

#include "common/memory_pressure.h"
#include <QSignalSpy>
#include <QTest>
#include <limits>
using namespace docview;
namespace {
constexpr quint64 MiB = 1024ull * 1024;
constexpr quint64 GiB = 1024 * MiB;
constexpr MemorySample Low{8 * GiB, 512 * MiB, true};
constexpr MemorySample Critical{8 * GiB, 128 * MiB, true};
constexpr MemorySample Healthy{8 * GiB, 2 * GiB, true};
} // namespace
class MemoryPressureTests : public QObject {
Q_OBJECT
private slots:
void parsesAvailableRatherThanFree();
void rejectsMalformedSamples_data();
void rejectsMalformedSamples();
void sustainedPressureAdvancesInOrder();
void criticalPressureStillAdvancesOneStage();
void thresholdBoundaries();
void recoveryRequiresHealthyConsecutiveSamples();
void invalidSamplePreservesStateAndBreaksStreaks();
void monitorInjectionAndOneSecondSource();
void nativeSampleIsConsistent();
};
void MemoryPressureTests::parsesAvailableRatherThanFree() {
const auto sample = parseLinuxMemoryInfo("MemFree: 1 kB\nMemAvailable:\t2048 kB\n"
"Other: ignored\nMemTotal: 8192 kB\n");
QVERIFY(sample.valid);
QCOMPARE(sample.totalBytes, 8 * MiB);
QCOMPARE(sample.availableBytes, 2 * MiB);
const auto empty = parseLinuxMemoryInfo("MemAvailable: 0 kB\nMemTotal: 1 kB");
QVERIFY(empty.valid);
QCOMPARE(empty.availableBytes, 0u);
const auto largest = std::numeric_limits<quint64>::max() / 1024;
const auto big =
parseLinuxMemoryInfo("MemTotal: " + QByteArray::number(largest) + " kB\nMemAvailable: 0 kB\n");
QVERIFY(big.valid);
QCOMPARE(big.totalBytes, largest * 1024);
}
void MemoryPressureTests::rejectsMalformedSamples_data() {
QTest::addColumn<QByteArray>("input");
QTest::newRow("empty") << QByteArray{};
QTest::newRow("no-total") << QByteArray("MemAvailable: 1 kB\n");
QTest::newRow("free-is-not-available") << QByteArray("MemTotal: 10 kB\nMemFree: 9 kB\n");
QTest::newRow("zero-total") << QByteArray("MemTotal: 0 kB\nMemAvailable: 0 kB\n");
QTest::newRow("available-exceeds-total") << QByteArray("MemTotal: 1 kB\nMemAvailable: 2 kB\n");
QTest::newRow("duplicate-total") << QByteArray("MemTotal: 2 kB\nMemTotal: 2 kB\nMemAvailable: 1 kB\n");
QTest::newRow("duplicate-available")
<< QByteArray("MemTotal: 2 kB\nMemAvailable: 1 kB\nMemAvailable: 1 kB\n");
QTest::newRow("byte-unit") << QByteArray("MemTotal: 2 B\nMemAvailable: 1 kB\n");
QTest::newRow("missing-unit") << QByteArray("MemTotal: 2\nMemAvailable: 1 kB\n");
QTest::newRow("negative") << QByteArray("MemTotal: 2 kB\nMemAvailable: -1 kB\n");
QTest::newRow("plus-sign") << QByteArray("MemTotal: +2 kB\nMemAvailable: 1 kB\n");
QTest::newRow("decimal") << QByteArray("MemTotal: 2.0 kB\nMemAvailable: 1 kB\n");
QTest::newRow("extra-field") << QByteArray("MemTotal: 2 kB extra\nMemAvailable: 1 kB\n");
QTest::newRow("integer-overflow")
<< QByteArray("MemTotal: 18446744073709551616 kB\nMemAvailable: 1 kB\n");
QTest::newRow("byte-overflow") << ("MemTotal: " +
QByteArray::number(std::numeric_limits<quint64>::max() / 1024 + 1) +
" kB\nMemAvailable: 1 kB\n");
QTest::newRow("nul") << (QByteArray("MemTotal: 2 kB\nMemAvailable: 1 kB\n") + '\0');
QTest::newRow("oversized") << QByteArray(64 * 1024 + 1, 'x');
}
void MemoryPressureTests::rejectsMalformedSamples() {
QFETCH(QByteArray, input);
QVERIFY(!parseLinuxMemoryInfo(input).valid);
}
void MemoryPressureTests::sustainedPressureAdvancesInOrder() {
MemoryPressurePolicy policy;
QCOMPARE(policy.observe(Low), MemoryPressureLevel::NoPrefetch);
for (const auto next :
{MemoryPressureLevel::Trim, MemoryPressureLevel::Reduced, MemoryPressureLevel::Stop}) {
const auto previous = policy.level();
for (int n = 1; n < MemoryPressurePolicy::SustainedSamplesPerStage; ++n)
QCOMPARE(policy.observe(Low), previous);
QCOMPARE(policy.observe(Low), next);
}
for (int n = 0; n < 30; ++n)
QCOMPARE(policy.observe(Low), MemoryPressureLevel::Stop);
}
void MemoryPressureTests::criticalPressureStillAdvancesOneStage() {
MemoryPressurePolicy policy;
for (const auto expected : {MemoryPressureLevel::NoPrefetch, MemoryPressureLevel::Trim,
MemoryPressureLevel::Reduced, MemoryPressureLevel::Stop})
QCOMPARE(policy.observe(Critical), expected);
QCOMPARE(policy.observe(Critical), MemoryPressureLevel::Stop);
}
void MemoryPressureTests::thresholdBoundaries() {
MemoryPressurePolicy absolute;
QCOMPARE(absolute.observe({64 * GiB, GiB, true}), MemoryPressureLevel::Normal);
QCOMPARE(absolute.observe({64 * GiB, GiB - 1, true}), MemoryPressureLevel::NoPrefetch);
QCOMPARE(absolute.observe({64 * GiB, 256 * MiB, true}), MemoryPressureLevel::NoPrefetch);
QCOMPARE(absolute.observe({64 * GiB, 256 * MiB - 1, true}), MemoryPressureLevel::Trim);
const quint64 tenPercent = (4 * GiB + 9) / 10;
MemoryPressurePolicy ratio;
QCOMPARE(ratio.observe({4 * GiB, tenPercent, true}), MemoryPressureLevel::Normal);
QCOMPARE(ratio.observe({4 * GiB, tenPercent - 1, true}), MemoryPressureLevel::NoPrefetch);
const quint64 threePercent = (4 * GiB * 3 + 99) / 100;
QCOMPARE(ratio.observe({4 * GiB, threePercent, true}), MemoryPressureLevel::NoPrefetch);
QCOMPARE(ratio.observe({4 * GiB, threePercent - 1, true}), MemoryPressureLevel::Trim);
MemoryPressurePolicy maximum;
const auto max = std::numeric_limits<quint64>::max();
QCOMPARE(maximum.observe({max, max, true}), MemoryPressureLevel::Normal);
QCOMPARE(maximum.observe({max, 0, true}), MemoryPressureLevel::NoPrefetch);
QCOMPARE(maximum.observe({max, 0, true}), MemoryPressureLevel::Trim);
}
void MemoryPressureTests::recoveryRequiresHealthyConsecutiveSamples() {
MemoryPressurePolicy policy;
for (int n = 0; n < 4; ++n)
policy.observe(Critical);
const MemorySample hysteresisBand{8 * GiB, GiB, true};
for (int n = 0; n < 20; ++n)
QCOMPARE(policy.observe(hysteresisBand), MemoryPressureLevel::Stop);
for (int n = 1; n < MemoryPressurePolicy::RecoverySamples; ++n)
QCOMPARE(policy.observe(Healthy), MemoryPressureLevel::Stop);
QCOMPARE(policy.observe(hysteresisBand), MemoryPressureLevel::Stop);
for (int n = 1; n < MemoryPressurePolicy::RecoverySamples; ++n)
QCOMPARE(policy.observe(Healthy), MemoryPressureLevel::Stop);
QCOMPARE(policy.observe(Healthy), MemoryPressureLevel::Normal);
QCOMPARE(policy.observe(Low), MemoryPressureLevel::NoPrefetch);
// Either margin can establish recovery on small and large memory systems.
MemoryPressurePolicy large;
large.observe({64 * GiB, 128 * MiB, true});
for (int n = 0; n < MemoryPressurePolicy::RecoverySamples; ++n)
large.observe({64 * GiB, 1536 * MiB, true});
QCOMPARE(large.level(), MemoryPressureLevel::Normal);
MemoryPressurePolicy small;
small.observe({4 * GiB, 128 * MiB, true});
for (int n = 0; n < MemoryPressurePolicy::RecoverySamples; ++n)
small.observe({4 * GiB, (4 * GiB * 15 + 99) / 100, true});
QCOMPARE(small.level(), MemoryPressureLevel::Normal);
}
void MemoryPressureTests::invalidSamplePreservesStateAndBreaksStreaks() {
MemoryPressurePolicy policy;
policy.observe(Low);
for (int n = 1; n < MemoryPressurePolicy::SustainedSamplesPerStage; ++n)
policy.observe(Low);
for (const MemorySample invalid : {MemorySample{}, MemorySample{0, 0, true}, MemorySample{1, 2, true},
MemorySample{8 * GiB, 2 * GiB, false}})
QCOMPARE(policy.observe(invalid), MemoryPressureLevel::NoPrefetch);
QCOMPARE(policy.observe(Low), MemoryPressureLevel::NoPrefetch);
for (int n = 1; n < MemoryPressurePolicy::RecoverySamples; ++n)
policy.observe(Healthy);
QCOMPARE(policy.observe({}), MemoryPressureLevel::NoPrefetch);
QCOMPARE(policy.observe(Healthy), MemoryPressureLevel::NoPrefetch);
}
void MemoryPressureTests::monitorInjectionAndOneSecondSource() {
int calls = 0;
MemoryPressureMonitor monitor(nullptr, [&] {
++calls;
return Critical;
});
QSignalSpy changed(&monitor, &MemoryPressureMonitor::levelChanged);
QVERIFY(!monitor.running());
QCOMPARE(calls, 0);
monitor.start();
QCOMPARE(calls, 1);
QCOMPARE(changed.size(), 1);
QCOMPARE(qvariant_cast<MemoryPressureLevel>(changed.last().first()), MemoryPressureLevel::NoPrefetch);
monitor.start();
QCOMPARE(calls, 1); // Idempotent start must not accelerate escalation.
QTRY_VERIFY_WITH_TIMEOUT(calls >= 2, 1500);
monitor.stop();
QVERIFY(!monitor.running());
QCOMPARE(monitor.level(), MemoryPressureLevel::Trim);
monitor.observeSample(Critical);
monitor.observeSample(Critical);
QCOMPARE(monitor.level(), MemoryPressureLevel::Stop);
QCOMPARE(changed.size(), 4);
monitor.observeSample({});
QCOMPARE(changed.size(), 4);
for (int n = 0; n < MemoryPressurePolicy::RecoverySamples; ++n)
monitor.observeSample(Healthy);
QCOMPARE(changed.size(), 5);
QCOMPARE(qvariant_cast<MemoryPressureLevel>(changed.last().first()), MemoryPressureLevel::Normal);
}
void MemoryPressureTests::nativeSampleIsConsistent() {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
const auto sample = readSystemMemory();
QVERIFY(sample.valid);
QVERIFY(sample.totalBytes > 0);
QVERIFY(sample.availableBytes <= sample.totalBytes);
#else
QVERIFY(!readSystemMemory().valid);
#endif
}
QTEST_GUILESS_MAIN(MemoryPressureTests)
#include "test_memory_pressure.moc"