LUMIERA.clone/tests/vault/mem/engine-buffer-manager-test.cpp
Ichthyostega 0567633bd9 BufferProvider: add implementation for the asynchronous hand-over
...and define a test case, to perform the typical usage sequence
within a lot of threads in parallel.
2026-05-13 01:47:51 +02:00

277 lines
9.3 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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-metadata.hpp"
#include "vault/mem/engine-buffer-manager.hpp"
#include "vault/mem/engine-buffer-allocator.hpp"
#include "lib/scoped-collection.hpp"
#include "lib/iter-explorer.hpp"
//#include "lib/depend-inject.hpp"
#include "lib/thread.hpp"
//#include "lib/error.hpp"
#include "lib/symbol.hpp"
#include "test/diagnostic-output.hpp"////////////TODO
#include <algorithm>
//#include <thread>
#include <array>
using lib::Thread;
using lib::Literal;
using lib::explore;
//using lib::ScopedCollection;
using std::this_thread::yield;
using std::this_thread::sleep_for;
using std::chrono::microseconds;
using std::chrono_literals::operator ""ms;
namespace vault {
namespace mem {
namespace test {
// using LERR_(LOGIC);
// using LERR_(LIFECYCLE);
namespace { // Test helper
const size_t ALLOC_REQ = 1024*1024;
const size_t NUM_THREADS = 100;
// template<typename X>
// Buff*
// mark_as_Buffer(X& something)
// {
// return reinterpret_cast<Buff*> (std::addressof(something));
// }
//
}
/*****************************************************************************//**
* @test Show how the central buffer manager of the Render Engine can be accessed.
*/
class EngineBufferManager_test : public Test
{
virtual void
run (Arg)
{
seedRand();
demonstrate_AllocatorInterface();
verify_syncRequest();
verify_asyncRequest();
verify_lulz();
}
/** @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()
{
using Allo = HeapBufferAllocator;
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....
auto doooh = new(a1.mem) f128{std::numeric_limits<f128>::max()};
*doooh /= 2;
CHECK (2 == std::numeric_limits<f128>::max() / *doooh);
allo.deallocate (a1);
allo.deallocate (a2);
}
/** @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).
*/
void
verify_syncRequest()
{
EngineBufferManager manager;
CHECK (watch(manager).isEmpty());
Alloc alloc = manager.requestAllocation (555);
CHECK (not alloc.empty());
CHECK (alloc.siz >= 555);
CHECK (alloc.siz == watch(manager).bytesAllocd());
CHECK (alloc.siz == watch(manager).bytesLeased());
CHECK (alloc.siz % HeapBufferAllocator::TILE_SIZ == 0);
// can use that memory
char* buf = reinterpret_cast<char*> (alloc.mem);
string ranS{lib::randStr(555-1)};
auto* p = std::copy (ranS.begin(), ranS.end(), buf);
CHECK (ranS.length() == size_t(p - buf));
*p = '\0';
CHECK (ranS == buf);
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());
}
/** @test show that an working allocation request passes thread boundaries */
void
verify_asyncRequest()
{
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();
}
};
EngineBufferManager manager;
using WorkBuff = std::array<uint64_t, ALLOC_REQ / sizeof(uint64_t)>;
std::atomic_int cnt{0};
auto workSeq = [&, gen = makeRandGen()]// local random generator per thread
() mutable
{// sequence of actions within a worker thread
int i=++cnt;
LocalTestPool myPool;
CHECK (myPool.empty());
manager.async_requestAllocation (myPool, ALLOC_REQ);
// meanwhile the worker does some other "work"
uint delay = 100 + gen.i(800);
sleep_for (microseconds(delay));
SHOW_EXPR(delay)
Alloc alloc = myPool.retrieveAlloc();
if (alloc.empty())
// they let us down ...
// insist harder to be serviced thusly
alloc = manager.requestAllocation (ALLOC_REQ);
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(); });
sleep_for (10ms);
// pass allocation back to the central hub
manager.supply (alloc);
SHOW_EXPR(i)
};
using Threads = lib::ScopedCollection<Thread>;
auto startThread = [&](auto& storage){ storage.template create<Thread> (workSeq); };
// Start a collection of workers....
Threads threads{NUM_THREADS, startThread};
// wait for all threads to finish...
while (explore(threads).has_any())
yield();
// all the allocations sent back by the workers
// should sit in the EngineBufferManager's input queue
SHOW_EXPR(watch(manager).numAllocs());
// CHECK (NUM_THREADS == watch(manager).numAllocs());
SHOW_EXPR(watch(manager).bytesLeased());
// CHECK (NUM_THREADS*ALLOC_REQ <= watch(manager).bytesLeased());
// process in-queue and retrieve all allocations..
manager.processPendingRequests();
SHOW_EXPR(watch(manager).numAllocs());
SHOW_EXPR(watch(manager).bytesLeased());
// CHECK (0 == watch(manager).bytesLeased());
}
/** @test
*/
void
verify_lulz()
{
}
};
/** Register this test class... */
LAUNCHER (EngineBufferManager_test, "unit engine");
}}} // namespace vault::mem::test