/* LocalMemPool(Test) - verify low-level handling of buffer blocks in a local pool Copyright (C) 2026, Hermann Vosseler   **Lumiera** is free software; you can redistribute it and/or modify it   under the terms of the GNU General Public License as published by the   Free Software Foundation; either version 2 of the License, or (at your   option) any later version. See the file COPYING for further details. * *****************************************************************/ /** @file local-mem-pool-test.cpp ** unit test \ref LocalMemPool_test */ #include "test/run.hpp" #include "test/test-helper.hpp" #include "vault/mem/local-mem-pool.hpp" #include "lib/iter-stack.hpp" #include "lib/format-util.hpp" using util::join; namespace vault { namespace mem { namespace test { namespace { // Test helper inline constexpr Buff* fake_Buffer(size_t id) { return reinterpret_cast (id); } Buff* const MEM1 = fake_Buffer(1); Buff* const MEM2 = fake_Buffer(2); Buff* const MEM3 = fake_Buffer(3); const size_t SIZ10{10}; const size_t SIZ20{20}; const size_t SIZ30{30}; Buff* const MEM11 = fake_Buffer(11); Buff* const MEM12 = fake_Buffer(12); Buff* const MEM13 = fake_Buffer(13); Buff* const MEM21 = fake_Buffer(21); Buff* const MEM31 = fake_Buffer(31); Buff* const MEM32 = fake_Buffer(32); Buff* const MEM33 = fake_Buffer(33); } /**********************************************************************//** * @test verify handling of prospective buffer allocations in a local pool. * - demonstrate usage of the API * - show how allocations are selected and discarded * @remark this covers the mechanics how allocation entries are * managed in the pool, without any infrastructure and * notably without the connection to a global manager. */ class LocalMemPool_test : public Test { virtual void run (Arg) { simpleUse(); verify_matchAlloc(); verify_selectAlloc(); } /** @test show how to place an allocation into the pool */ void simpleUse() { LocalMemPool pool; pool.add (MEM1, SIZ20); CHECK (pool.canServe(SIZ20)); CHECK (pool.canServe(SIZ10)); auto [mem,siz] = pool.retrieve (SIZ10); CHECK (MEM1 == mem); CHECK (SIZ20 == siz); CHECK (not pool.canServe(SIZ10)); pool.reAdd (MEM1); CHECK (pool.canServe(SIZ20)); } /** @test allocations are accounted by size * - new memory is posted through an in-queue * - for each request, the closest matching allocation is handed out * - a partial clean-up removes unused / bad matching allocations first * - a complete clean-up clears out the pool */ void verify_matchAlloc() { LocalMemPool pool; CHECK (watch(pool).isEmpty()); // Feed new allocations through the in-queue... pool.add (MEM1, SIZ10); CHECK (watch(pool).cnt(SIZ10) == 0); // new allotment not ingested yet,.... CHECK (not watch(pool).isEmpty()); // however, there is an entry in the in-queue CHECK (pool.canServe(SIZ10)); // query causes a scan and retrieval from the in-queu CHECK (watch(pool).cnt(SIZ10) == 1); CHECK (not watch(pool).isEmpty()); pool.add (MEM2, SIZ20); pool.add (MEM3, SIZ30); CHECK (pool.canServe(SIZ20)); CHECK (pool.canServe(SIZ10)); CHECK (watch(pool).size() == 3); CHECK (watch(pool).cnt(SIZ10) == 1); CHECK (watch(pool).cnt(SIZ20) == 1); CHECK (watch(pool).cnt(SIZ30) == 1); CHECK (watch(pool).cntFree() == 3); // Request some allocations from the pool.... auto [m1,s1] = pool.retrieve (SIZ10 -2); CHECK (SIZ10 == s1); CHECK (MEM1 == m1); CHECK (watch(pool).cntFree() == 2); auto [m2,s2] = pool.retrieve (SIZ20); CHECK (SIZ20 == s2); CHECK (MEM2 == m2); CHECK (watch(pool).size() == 3); CHECK (watch(pool).cntFree() == 1); CHECK (pool.canServe(SIZ10)); // only the SIZ30-allocation is available from the pool CHECK (pool.canServe(SIZ20)); // which however could serve also smaller requests... CHECK (pool.canServe(SIZ30)); // Return allocations back into the pool pool.reAdd (m2); pool.reAdd (m1); CHECK (watch(pool).cntFree() == 3); // Shrink / clean-up will return allocations through a consumer lib::IterQueue returned; auto returnAlloc = [&](auto, size_t siz){ returned.feed (siz); }; uint cnt = pool.cleanup(0.5 ,returnAlloc); // heuristic partial clean-up CHECK (cnt == 1); CHECK (join(returned) == "30"_expect); // the SIZ30 allocation was not used yet, and thus returned first CHECK (watch(pool).cntFree() == 2); CHECK (watch(pool).size() == 2); cnt = 0; cnt = pool.yield(2, SIZ20, returnAlloc); // explicit request to return two SIZ20 allocations... CHECK (cnt == 1); // yet there is only one such block in the pool CHECK (join(returned) == "30, 20"_expect); CHECK (watch(pool).cnt(SIZ10) == 1); CHECK (watch(pool).cnt(SIZ20) == 0); CHECK (watch(pool).cnt(SIZ30) == 0); CHECK (watch(pool).cntFree() == 1); CHECK (watch(pool).size() == 1); // add a further allocation with SIZ30 CHECK (not pool.canServe(SIZ30)); pool.add (MEM3, SIZ30); CHECK (watch(pool).size() == 1); CHECK ( pool.canServe(SIZ30)); CHECK (watch(pool).size() == 2); cnt = 0; cnt = pool.cleanup(1.0 ,returnAlloc); // complete clean-up requested CHECK (cnt == 2); CHECK (join(returned) == "30, 20, 30, 10"_expect); CHECK (watch(pool).isEmpty()); } /** @test the pool picks and discards allocations heuristically: * - allocations are only marked as reserved when they are * pretty close above the requested amount of memory * - however, all allocations in the pool are used * to handle requests, irrespective of reservation status * - the prime criterion to select an entry is the match quality * - yet frequently used blocks are preferred for a rather poor match * - each usage adds to the score, but better matches score higher * - but when an entry is too small to be used, it is marked with a penalty * - entries with low score are cleaned and returned first, while * successful entries with high score are retained */ void verify_selectAlloc() { LocalMemPool pool; pool.add (MEM11, SIZ10); pool.add (MEM31, SIZ30); pool.add (MEM32, SIZ30); pool.add (MEM33, SIZ30); uint cnt = pool.reserve(4, SIZ10); // request to reserve 4 blocks of SIZ10 CHECK (cnt == 1); // yet only one block is a good match and thus reserved cnt = pool.reserve(2, SIZ30-5); CHECK (cnt == 2); // these two reservation requests can be satisfied cnt = pool.reserve(2, SIZ10); CHECK (cnt == 0); // but the pool is now unable to reserve more SIZ10 (without wasting memory) auto [m1,s1] = pool.retrieve (SIZ10); // actual allocation requests are always served however, if possible at all auto [m2,s2] = pool.retrieve (SIZ10); auto [m3,s3] = pool.retrieve (SIZ10); auto [m4,s4] = pool.retrieve (SIZ20+SIZ20); // this one can certainly not be satisfied CHECK (m1 == MEM11); // the first request gets the good match CHECK (m2 == MEM33); // the following ones are satisfied with an over-allocation CHECK (m3 == MEM32); CHECK (not m4); CHECK (watch(pool).cntFree() == 1); CHECK (watch(pool).size() == 4); // return all blocks... pool.reAdd (m1); pool.reAdd (m2); pool.reAdd (m3); VERIFY_FAIL ("unknown allocation", pool.reAdd (m4)) VERIFY_FAIL ("unknown allocation", pool.reAdd (fake_Buffer(12345))) VERIFY_FAIL ("not marked as used", pool.reAdd (MEM31)) CHECK (watch(pool).cntFree() == 4); // all entries in the pool were scored.... CHECK (watch(pool).getScore(MEM31) == -2); // never used and -2 penalty for being once to small to be useful (for SIZ20+SIZ20) CHECK (watch(pool).getScore(MEM32) == 3); // these were used once, but as over-allocation using only 1/3 of their size SIZ30 CHECK (watch(pool).getScore(MEM33) == 3); CHECK (watch(pool).getScore(MEM11) == 10); // this one was used as a perfect match and thus got the full score // this score determines how they are used further.... pool.add (MEM12, SIZ10); auto [m5,s5] = pool.retrieve (SIZ10-5); auto [m6,s6] = pool.retrieve (SIZ10); auto [m7,s7] = pool.retrieve (SIZ10); auto [m8,s8] = pool.retrieve (SIZ20); auto [m9,s9] = pool.retrieve (SIZ20); CHECK (m5 == MEM11); // used first because it matches and has the best score CHECK (m6 == MEM12); // used next because it's a perfect match CHECK (m7 == MEM33); // this is a bad match, but it's the one left with the largest score CHECK (m8 == MEM32); // the next best score is used to satisfy this request for SIZ20 CHECK (m9 == MEM31); // only one left (which has the worst score because it was never used yet) CHECK (watch(pool).cntFree() == 0); CHECK (watch(pool).size() == 5); pool.reAdd (m8); pool.reAdd (m7); pool.reAdd (m6); pool.reAdd (m5); CHECK (watch(pool).cntFree() == 4); CHECK (watch(pool).getScore(MEM11) == 15); // got +5 score because the match quality was only 50% (SIZ10 but only 5 needed) CHECK (watch(pool).getScore(MEM12) == 10); // this one was new, but got a +10 score due to the perfect match CHECK (watch(pool).getScore(MEM31) == 4); // this had -1 score but got +6 due to 60% match quality (SIZ30 but SIZ20 requested) CHECK (watch(pool).getScore(MEM32) == 9); // previous score was +3 and it likewise got +6 CHECK (watch(pool).getScore(MEM33) == 6); // this had also +3 and was first in the list, but got only +3 (SIZ30 used to satisfy SIZ10) // entries removed from pool... lib::IterQueue returned; auto returnAlloc = [&](Buff* mem, size_t){ returned.feed (size_t(mem)); }; // perform a rather aggressive clean-up cnt = pool.cleanup(0.7 ,returnAlloc); // max score is 15, so anything below 0.7*15 ≡ 10 will be expunged CHECK (cnt == 3); CHECK (join(returned) == "12, 33, 32"_expect); // MEM12, MEM33 and MEM32 returned, since their score as <= 10 CHECK (watch(pool).cntFree() == 1); CHECK (watch(pool).size() == 2); CHECK (not watch(pool).isFree(MEM31)); // Note: MEM31 is still in use and thus remains in pool, even with that low score of 4 CHECK (watch(pool).getScore(MEM11) == 5); // Scores were reduced accordingly, cutting away a the »kill level« of +10 CHECK (watch(pool).getScore(MEM31) == -6); pool.reAdd (MEM31); auto [mX,sX] = pool.retrieve (SIZ20); CHECK (mX == MEM31); CHECK (sX == SIZ30); pool.reAdd (MEM31); CHECK (watch(pool).cntFree() == 2); CHECK (watch(pool).getScore(MEM11) == 3); // was penalised with -2 since MEM11 was too small to satisfy a SIZ20 request CHECK (watch(pool).getScore(MEM31) == 0); // while MEM31 got a score of +6 for a usage with 60% match quality (SIZ30 used for SIZ20) // shut down the pool... cnt = pool.cleanup(1.0 ,returnAlloc); CHECK (cnt == 2); CHECK (join(returned) == "12, 33, 32, 31, 11"_expect); CHECK (pool.empty()); } }; /** Register this test class... */ LAUNCHER (LocalMemPool_test, "unit engine"); }}} // namespace vault::mem::test