2026-04-27 23:45:05 +02:00
|
|
|
|
/*
|
|
|
|
|
|
EngineBufferManager(Test) - cover the interface to a central buffer manager
|
|
|
|
|
|
|
|
|
|
|
|
Copyright (C)
|
|
|
|
|
|
2026, Hermann Vosseler <Ichthyostega@web.de>
|
|
|
|
|
|
|
|
|
|
|
|
**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 engine-buffer-manager-test.cpp
|
|
|
|
|
|
** unit test \ref EngineBufferManager_test
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "test/run.hpp"
|
|
|
|
|
|
#include "test/test-helper.hpp"
|
|
|
|
|
|
#include "vault/mem/engine-buffer-manager.hpp"
|
2026-05-08 01:58:36 +02:00
|
|
|
|
#include "vault/mem/engine-buffer-allocator.hpp"
|
2026-05-13 01:47:51 +02:00
|
|
|
|
#include "lib/scoped-collection.hpp"
|
|
|
|
|
|
#include "lib/iter-explorer.hpp"
|
2026-05-13 19:57:50 +02:00
|
|
|
|
#include "lib/sync-barrier.hpp"
|
2026-05-13 01:47:51 +02:00
|
|
|
|
#include "lib/thread.hpp"
|
2026-05-12 19:40:45 +02:00
|
|
|
|
#include "lib/symbol.hpp"
|
2026-04-27 23:45:05 +02:00
|
|
|
|
|
2026-05-12 19:40:45 +02:00
|
|
|
|
#include <algorithm>
|
2026-05-13 19:57:50 +02:00
|
|
|
|
#include <string>
|
2026-05-13 01:47:51 +02:00
|
|
|
|
#include <array>
|
2026-04-27 23:45:05 +02:00
|
|
|
|
|
2026-05-13 01:47:51 +02:00
|
|
|
|
using lib::explore;
|
2026-05-13 19:57:50 +02:00
|
|
|
|
using lib::Literal;
|
|
|
|
|
|
using lib::Thread;
|
|
|
|
|
|
using lib::SyncBarrier;
|
2026-05-13 01:47:51 +02:00
|
|
|
|
using std::this_thread::yield;
|
|
|
|
|
|
using std::this_thread::sleep_for;
|
|
|
|
|
|
using std::chrono::microseconds;
|
|
|
|
|
|
using std::chrono_literals::operator ""ms;
|
2026-04-27 23:45:05 +02:00
|
|
|
|
|
|
|
|
|
|
namespace vault {
|
|
|
|
|
|
namespace mem {
|
|
|
|
|
|
namespace test {
|
|
|
|
|
|
|
2026-05-13 19:57:50 +02:00
|
|
|
|
namespace { // Test parameters......
|
|
|
|
|
|
const size_t ALLOC_REQ = 1024*1024; ///< use test-allocation request of 1 MiB size
|
|
|
|
|
|
const size_t NUM_THREADS = 100; ///< press concurrently
|
2026-04-27 23:45:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*****************************************************************************//**
|
|
|
|
|
|
* @test Show how the central buffer manager of the Render Engine can be accessed.
|
|
|
|
|
|
*/
|
|
|
|
|
|
class EngineBufferManager_test : public Test
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
virtual void
|
|
|
|
|
|
run (Arg)
|
|
|
|
|
|
{
|
|
|
|
|
|
demonstrate_AllocatorInterface();
|
2026-05-12 19:40:45 +02:00
|
|
|
|
verify_syncRequest();
|
|
|
|
|
|
verify_asyncRequest();
|
2026-04-27 23:45:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @test show how to allocate buffer memory
|
|
|
|
|
|
* with the underlying base allocator.
|
|
|
|
|
|
* @remark 4/2026 for the first integration of the Render Engine,
|
|
|
|
|
|
* a simplified implementation is used that just delegates
|
|
|
|
|
|
* to the regular heap allocator (which actually performs
|
|
|
|
|
|
* quite well on modern C++ STDLib). The plan is to
|
|
|
|
|
|
* use some kind of tiling pool allocator eventually,
|
|
|
|
|
|
* contingent on the actual performance observations.
|
|
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
demonstrate_AllocatorInterface()
|
|
|
|
|
|
{
|
2026-05-08 01:58:36 +02:00
|
|
|
|
using Allo = HeapBufferAllocator;
|
2026-04-27 23:45:05 +02:00
|
|
|
|
CHECK (lib::allo::is_Stateless_v<Allo>);
|
|
|
|
|
|
|
|
|
|
|
|
Allo allo;
|
|
|
|
|
|
auto a1 = allo.allocate (55);
|
|
|
|
|
|
auto a2 = allo.allocate (555);
|
|
|
|
|
|
CHECK (a1.mem);
|
|
|
|
|
|
CHECK (a2.mem);
|
|
|
|
|
|
CHECK (a1.siz == Allo::TILE_SIZ);
|
|
|
|
|
|
CHECK (a2.siz % Allo::TILE_SIZ == 0);
|
|
|
|
|
|
CHECK (a2.siz > 555);
|
|
|
|
|
|
|
|
|
|
|
|
// can use that memory without blowing up....
|
2026-09-21 01:05:53 +02:00
|
|
|
|
auto doooh = new(a1.mem) lflp{std::numeric_limits<lflp>::max()};
|
2026-04-27 23:45:05 +02:00
|
|
|
|
*doooh /= 2;
|
2026-09-21 01:05:53 +02:00
|
|
|
|
CHECK (2 == std::numeric_limits<lflp>::max() / *doooh);
|
2026-04-27 23:45:05 +02:00
|
|
|
|
|
|
|
|
|
|
allo.deallocate (a1);
|
|
|
|
|
|
allo.deallocate (a2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-12 23:52:25 +02:00
|
|
|
|
/** @test show that a direct request for allocation is honored
|
|
|
|
|
|
* - request some memory
|
|
|
|
|
|
* - read and write to that memory
|
|
|
|
|
|
* - return all leased allocations
|
|
|
|
|
|
* - verify the diagnostic meshes up with the allocation
|
|
|
|
|
|
* @remark in principle it is not possible to prove that some
|
|
|
|
|
|
* memory allocation actually works, other than checking
|
|
|
|
|
|
* that access to the given memory is possible, because
|
|
|
|
|
|
* the fact that no memory corruption took place is
|
|
|
|
|
|
* only semi-decidable (you can show a corruption event,
|
|
|
|
|
|
* but not the absence of any such an event).
|
|
|
|
|
|
*/
|
2026-04-27 23:45:05 +02:00
|
|
|
|
void
|
2026-05-12 19:40:45 +02:00
|
|
|
|
verify_syncRequest()
|
|
|
|
|
|
{
|
|
|
|
|
|
EngineBufferManager manager;
|
2026-05-12 23:52:25 +02:00
|
|
|
|
CHECK (watch(manager).isEmpty());
|
2026-05-12 19:40:45 +02:00
|
|
|
|
|
|
|
|
|
|
Alloc alloc = manager.requestAllocation (555);
|
|
|
|
|
|
CHECK (not alloc.empty());
|
|
|
|
|
|
CHECK (alloc.siz >= 555);
|
|
|
|
|
|
|
2026-05-12 23:52:25 +02:00
|
|
|
|
CHECK (alloc.siz == watch(manager).bytesAllocd());
|
|
|
|
|
|
CHECK (alloc.siz == watch(manager).bytesLeased());
|
|
|
|
|
|
CHECK (alloc.siz % HeapBufferAllocator::TILE_SIZ == 0);
|
2026-05-12 19:40:45 +02:00
|
|
|
|
|
|
|
|
|
|
// can use that memory
|
|
|
|
|
|
char* buf = reinterpret_cast<char*> (alloc.mem);
|
2026-05-13 19:57:50 +02:00
|
|
|
|
std::string ranS{lib::randStr(555-1)};
|
2026-05-12 19:40:45 +02:00
|
|
|
|
auto* p = std::copy (ranS.begin(), ranS.end(), buf);
|
2026-05-12 23:52:25 +02:00
|
|
|
|
CHECK (ranS.length() == size_t(p - buf));
|
2026-05-12 19:40:45 +02:00
|
|
|
|
*p = '\0';
|
|
|
|
|
|
CHECK (ranS == buf);
|
2026-05-12 23:52:25 +02:00
|
|
|
|
|
|
|
|
|
|
CHECK (1 == watch(manager).numAllocs());
|
|
|
|
|
|
Alloc a2 = manager.requestAllocation (1'000'000);
|
|
|
|
|
|
CHECK (not a2.empty());
|
|
|
|
|
|
CHECK (a2.siz >= 1e6);
|
|
|
|
|
|
CHECK (2 == watch(manager).numAllocs());
|
|
|
|
|
|
CHECK (1e6+555 < watch(manager).bytesLeased());
|
|
|
|
|
|
|
|
|
|
|
|
manager.supply (a2);
|
|
|
|
|
|
manager.processPendingRequests();
|
|
|
|
|
|
CHECK (1 == watch(manager).numAllocs());
|
|
|
|
|
|
CHECK (1e6 > watch(manager).bytesLeased());
|
|
|
|
|
|
manager.supply (alloc);
|
|
|
|
|
|
manager.processPendingRequests();
|
|
|
|
|
|
CHECK (0 == watch(manager).numAllocs());
|
|
|
|
|
|
CHECK (0 == watch(manager).bytesLeased());
|
2026-05-12 19:40:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 19:57:50 +02:00
|
|
|
|
/** @test show a working allocation request passed over thread boundaries...
|
|
|
|
|
|
* - use a simplified »local pool« that actually just provides an in-queue
|
|
|
|
|
|
* - start a lot of threads that perform the typical allocation sequence
|
|
|
|
|
|
* + first send an asynchronous allocation request
|
|
|
|
|
|
* + some delay while doing other stuff (here: sleep)
|
|
|
|
|
|
* + check if allocation has been serviced
|
|
|
|
|
|
* + if not, then perform global allocation servicing in this worker
|
|
|
|
|
|
* + allocation must be available now, can use it
|
|
|
|
|
|
* + when done, send allocation back to the global pool
|
|
|
|
|
|
*/
|
2026-05-12 19:40:45 +02:00
|
|
|
|
void
|
|
|
|
|
|
verify_asyncRequest()
|
2026-04-27 23:45:05 +02:00
|
|
|
|
{
|
2026-05-13 01:47:51 +02:00
|
|
|
|
struct LocalTestPool
|
|
|
|
|
|
: AllocReceiver
|
|
|
|
|
|
{
|
|
|
|
|
|
Alloc
|
|
|
|
|
|
retrieveAlloc()
|
|
|
|
|
|
{
|
|
|
|
|
|
Alloc received{nullptr, 0};
|
|
|
|
|
|
inQueue_.pop (received); // Note: no-op if queue is empty
|
|
|
|
|
|
return received;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool
|
|
|
|
|
|
empty() const
|
|
|
|
|
|
{
|
|
|
|
|
|
return inQueue_.empty();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-05-13 19:57:50 +02:00
|
|
|
|
|
2026-05-13 01:47:51 +02:00
|
|
|
|
EngineBufferManager manager;
|
|
|
|
|
|
|
|
|
|
|
|
using WorkBuff = std::array<uint64_t, ALLOC_REQ / sizeof(uint64_t)>;
|
|
|
|
|
|
|
2026-05-13 19:57:50 +02:00
|
|
|
|
SyncBarrier afterThread{NUM_THREADS+1};
|
|
|
|
|
|
auto workSeq = [&]{ // sequence-of-actions-within-worker-thread-------------------
|
2026-05-13 01:47:51 +02:00
|
|
|
|
LocalTestPool myPool;
|
|
|
|
|
|
CHECK (myPool.empty());
|
2026-05-13 19:57:50 +02:00
|
|
|
|
|
2026-05-13 01:47:51 +02:00
|
|
|
|
manager.async_requestAllocation (myPool, ALLOC_REQ);
|
2026-05-13 19:57:50 +02:00
|
|
|
|
|
|
|
|
|
|
// meanwhile the worker does some other "work"...
|
|
|
|
|
|
auto gen = makeRandGen(); // local random generator per thread
|
|
|
|
|
|
uint delay = 100 + gen.i(900);
|
2026-05-13 01:47:51 +02:00
|
|
|
|
sleep_for (microseconds(delay));
|
|
|
|
|
|
|
|
|
|
|
|
Alloc alloc = myPool.retrieveAlloc();
|
|
|
|
|
|
if (alloc.empty())
|
2026-05-13 19:57:50 +02:00
|
|
|
|
{ // they let us down ...
|
|
|
|
|
|
// insist harder to be serviced thusly
|
|
|
|
|
|
manager.processPendingRequests();
|
|
|
|
|
|
alloc = myPool.retrieveAlloc();
|
|
|
|
|
|
// Explanation: since the async_requestAllocation() »happens-before« the processPendingRequests()
|
|
|
|
|
|
} // our request *must* have been processed and dispatched now, unless there was a failure
|
2026-05-13 01:47:51 +02:00
|
|
|
|
|
|
|
|
|
|
CHECK (not alloc.empty());
|
|
|
|
|
|
CHECK (ALLOC_REQ <= alloc.siz);
|
|
|
|
|
|
// can use that allocation for some "serious work"...
|
|
|
|
|
|
auto content = new(alloc.mem) WorkBuff;
|
|
|
|
|
|
std::ranges::generate (*content, [&]{ return gen.u64(); });
|
2026-05-13 19:57:50 +02:00
|
|
|
|
|
|
|
|
|
|
// coordinate end of all threads
|
|
|
|
|
|
afterThread.sync();
|
2026-05-13 01:47:51 +02:00
|
|
|
|
|
|
|
|
|
|
// pass allocation back to the central hub
|
|
|
|
|
|
manager.supply (alloc);
|
2026-05-13 19:57:50 +02:00
|
|
|
|
CHECK (myPool.empty()); // no further pending / duplicate Alloc in-queue
|
2026-05-13 01:47:51 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Start a collection of workers....
|
2026-05-14 19:30:22 +02:00
|
|
|
|
using Threads = lib::ScopedCollection<Thread>;
|
|
|
|
|
|
Threads threads{NUM_THREADS, Threads::fill(workSeq)};
|
2026-05-13 01:47:51 +02:00
|
|
|
|
|
2026-05-13 19:57:50 +02:00
|
|
|
|
// after that barrier, all threads send back their allocation
|
|
|
|
|
|
afterThread.sync();
|
|
|
|
|
|
|
2026-05-13 01:47:51 +02:00
|
|
|
|
// wait for all threads to finish...
|
|
|
|
|
|
while (explore(threads).has_any())
|
|
|
|
|
|
yield();
|
|
|
|
|
|
|
|
|
|
|
|
// all the allocations sent back by the workers
|
2026-05-13 19:57:50 +02:00
|
|
|
|
// should sit now in the EngineBufferManager's input queue
|
|
|
|
|
|
CHECK (NUM_THREADS == watch(manager).numAllocs());
|
|
|
|
|
|
CHECK (NUM_THREADS*ALLOC_REQ <= watch(manager).bytesLeased());
|
2026-05-13 01:47:51 +02:00
|
|
|
|
|
|
|
|
|
|
// process in-queue and retrieve all allocations..
|
|
|
|
|
|
manager.processPendingRequests();
|
2026-05-13 19:57:50 +02:00
|
|
|
|
CHECK (0 == watch(manager).bytesLeased());
|
2026-04-27 23:45:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Register this test class... */
|
|
|
|
|
|
LAUNCHER (EngineBufferManager_test, "unit engine");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}}} // namespace vault::mem::test
|