2026-05-28 02:18:06 +02:00
|
|
|
|
/*
|
|
|
|
|
|
BufferProviderLoad(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 BufferProviderLoad_test
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "test/run.hpp"
|
|
|
|
|
|
#include "test/test-helper.hpp"
|
2026-05-29 23:46:20 +02:00
|
|
|
|
#include "test/microbenchmark.hpp"
|
2026-05-28 19:15:03 +02:00
|
|
|
|
#include "vault/mem/buffer-provider-setup.hpp"
|
|
|
|
|
|
#include "vault/mem/engine-buffer-metadata.hpp"
|
2026-05-28 02:18:06 +02:00
|
|
|
|
#include "vault/mem/engine-buffer-manager.hpp"
|
2026-05-28 19:15:03 +02:00
|
|
|
|
#include "vault/mem/local-buffer-stage.hpp"
|
|
|
|
|
|
#include "vault/mem/local-buffer-store.hpp"
|
2026-05-28 02:18:06 +02:00
|
|
|
|
#include "vault/gear/test-chain-load.hpp"
|
|
|
|
|
|
#include "lib/scoped-collection.hpp"
|
2026-05-28 19:15:03 +02:00
|
|
|
|
#include "lib/depend-inject.hpp"
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
#include "lib/iter-explorer.hpp"
|
2026-05-28 02:18:06 +02:00
|
|
|
|
#include "lib/sync-barrier.hpp"
|
2026-05-28 19:15:03 +02:00
|
|
|
|
#include "lib/iter-stack.hpp"
|
2026-05-28 02:18:06 +02:00
|
|
|
|
#include "lib/thread.hpp"
|
|
|
|
|
|
#include "lib/random.hpp"
|
|
|
|
|
|
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
using lib::Random;
|
2026-05-28 19:15:03 +02:00
|
|
|
|
using lib::Depend;
|
|
|
|
|
|
using lib::DependInject;
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
using lib::explore;
|
2026-05-28 02:18:06 +02:00
|
|
|
|
using lib::Thread;
|
|
|
|
|
|
using lib::SyncBarrier;
|
2026-05-28 19:15:03 +02:00
|
|
|
|
using lib::ScopedCollection;
|
2026-05-29 23:46:20 +02:00
|
|
|
|
using test::benchmarkTime;
|
2026-05-28 02:18:06 +02:00
|
|
|
|
using vault::gear::test::ComputationalLoad;
|
|
|
|
|
|
using std::this_thread::yield;
|
|
|
|
|
|
using std::this_thread::sleep_for;
|
|
|
|
|
|
using std::chrono::microseconds;
|
|
|
|
|
|
using std::chrono_literals::operator ""us;
|
|
|
|
|
|
using std::chrono_literals::operator ""ms;
|
|
|
|
|
|
|
|
|
|
|
|
namespace vault {
|
|
|
|
|
|
namespace mem {
|
|
|
|
|
|
namespace test {
|
|
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
namespace { // ========== Test Setup ==========
|
2026-05-28 19:15:03 +02:00
|
|
|
|
|
2026-05-29 23:46:20 +02:00
|
|
|
|
const size_t FULL_LOAD_CORES = 4; ///< workers to use for a _full load run_
|
2026-05-29 04:33:51 +02:00
|
|
|
|
const size_t MAX_SCALE = 5; ///< maximum scale step for randomised buffer types
|
2026-05-28 19:15:03 +02:00
|
|
|
|
|
2026-05-31 23:48:26 +02:00
|
|
|
|
size_t MANY_THREADS = 100; ///< press concurrently with an overload of threads
|
|
|
|
|
|
const size_t NOT_SO_MANY = 16; ///< a _light variant_ for use in the standard test suite
|
2026-05-28 19:15:03 +02:00
|
|
|
|
|
|
|
|
|
|
/** seed a per-thread random generator */
|
|
|
|
|
|
inline Random
|
2026-05-29 04:33:51 +02:00
|
|
|
|
chainedRandGen()
|
2026-05-28 19:15:03 +02:00
|
|
|
|
{
|
|
|
|
|
|
return Random{lib::seedFromDefaultGen()};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* BufferProvider implementation
|
|
|
|
|
|
* configured similarly to the real Render Engine.
|
|
|
|
|
|
* Using a distinct BufferStage and BufferStore (local pool)
|
|
|
|
|
|
* for each new thread, together with the global EngineBufferMetadata
|
|
|
|
|
|
* and EngineBufferManager, accessed through lib::Depend.
|
|
|
|
|
|
*/
|
2026-05-28 19:15:03 +02:00
|
|
|
|
class MultithreadBufferSetup
|
|
|
|
|
|
: public BufferProviderSetup
|
|
|
|
|
|
{
|
|
|
|
|
|
public:
|
|
|
|
|
|
MultithreadBufferSetup()
|
|
|
|
|
|
: BufferProviderSetup{*this}
|
|
|
|
|
|
{ }
|
|
|
|
|
|
|
|
|
|
|
|
auto buildStage() { return std::make_unique<LocalBufferStage>("BufferProviderLoad_test"); }
|
|
|
|
|
|
auto buildStore() { return std::make_unique<LocalBufferStore>(); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/** use a common selection of pre-configured buffer sizes */
|
2026-05-28 19:15:03 +02:00
|
|
|
|
using BuffTypes = ScopedCollection<BuffDescr>;
|
|
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/**************************************************//**
|
2026-05-28 19:15:03 +02:00
|
|
|
|
* Mock dependency injection context
|
|
|
|
|
|
* to emulate a Render Engine setup
|
2026-05-29 04:33:51 +02:00
|
|
|
|
* @remarks
|
|
|
|
|
|
* - MultithreadedBufferSetup is only used for this test
|
|
|
|
|
|
* and creates per-thread implementation backend instances
|
|
|
|
|
|
* - during the lifetime of such a Ctx instance in the local scope,
|
|
|
|
|
|
* the global EngineBufferMetadata- and EngineBufferManager-Singletons
|
|
|
|
|
|
* are shadowed by pristine new instances created temporarily
|
2026-05-28 19:15:03 +02:00
|
|
|
|
*/
|
|
|
|
|
|
struct Ctx
|
|
|
|
|
|
{
|
|
|
|
|
|
static Depend<MultithreadBufferSetup> bufferProvider;
|
|
|
|
|
|
static BuffTypes buffType_;
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
static size_t calibrated_;
|
2026-05-28 19:15:03 +02:00
|
|
|
|
|
|
|
|
|
|
// install mock instances for the relevant services
|
|
|
|
|
|
DependInject<EngineBufferMetadata>::Local<EngineBufferMetadata> metaHub;
|
|
|
|
|
|
DependInject<EngineBufferManager>::Local<EngineBufferManager> globalPool;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// static storage for the mock-engine entrance point
|
|
|
|
|
|
Depend<MultithreadBufferSetup> Ctx::bufferProvider;
|
|
|
|
|
|
BuffTypes Ctx::buffType_{MAX_SCALE};
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
size_t Ctx::calibrated_{0};
|
2026-05-28 02:18:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/* ============= Simulated Worker Load ============= */
|
|
|
|
|
|
|
2026-05-28 19:15:03 +02:00
|
|
|
|
struct WorkSetup
|
|
|
|
|
|
{
|
2026-05-29 04:33:51 +02:00
|
|
|
|
static constexpr uint NUM_JOBS = 10; ///< number of work-jobs to perform in this thread
|
|
|
|
|
|
static constexpr uint JOB_STEPS = 10; ///< number of processing steps in a single job
|
|
|
|
|
|
static constexpr uint STEP_SPREAD = 4; ///< max random spread of allocations / deallocations
|
|
|
|
|
|
static constexpr uint LOAD_SPREAD = 5; ///< max spread of »buffer types« to pick
|
|
|
|
|
|
static constexpr uint MAX_DELAY = 1000; ///< maximum delay in µs between jobs
|
2026-05-28 19:15:03 +02:00
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
static constexpr size_t BASE_BUFFSIZ = 100_KiB; ///< @note scaled by sizeof(size_t) * LOAD_SPREAD
|
2026-05-28 19:15:03 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-29 04:33:51 +02:00
|
|
|
|
* Simulation of the load produced by a worker thread.
|
|
|
|
|
|
* Performs a sequence of work actions, requesting
|
|
|
|
|
|
* buffer memory an causing computational load.
|
2026-05-28 19:15:03 +02:00
|
|
|
|
*/
|
|
|
|
|
|
template<class CONF = WorkSetup>
|
|
|
|
|
|
class DummyWorkerThread
|
|
|
|
|
|
: CONF
|
2026-05-29 04:33:51 +02:00
|
|
|
|
, SyncBarrier
|
2026-05-28 19:15:03 +02:00
|
|
|
|
, public Thread
|
|
|
|
|
|
{
|
2026-05-29 04:33:51 +02:00
|
|
|
|
ComputationalLoad load_;
|
|
|
|
|
|
/// local random generator per thread
|
|
|
|
|
|
Random rand_;
|
2026-05-28 19:15:03 +02:00
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/**********//**
|
|
|
|
|
|
* Thread-Main
|
|
|
|
|
|
*/
|
2026-05-28 19:15:03 +02:00
|
|
|
|
void
|
|
|
|
|
|
workActivity()
|
|
|
|
|
|
{
|
2026-05-29 04:33:51 +02:00
|
|
|
|
SyncBarrier::sync();
|
|
|
|
|
|
|
2026-05-28 19:15:03 +02:00
|
|
|
|
for (uint i=0; i<CONF::NUM_JOBS; ++i)
|
|
|
|
|
|
{
|
2026-05-29 04:33:51 +02:00
|
|
|
|
uint delay = rand_.i (CONF::MAX_DELAY);
|
2026-05-28 19:15:03 +02:00
|
|
|
|
sleep_for (microseconds(delay));
|
2026-05-29 04:33:51 +02:00
|
|
|
|
|
2026-05-28 19:15:03 +02:00
|
|
|
|
jobActivity();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Simulated calculation job.
|
2026-05-29 23:46:20 +02:00
|
|
|
|
* A random-walk-like memory load pattern is generated
|
|
|
|
|
|
* by claiming and releasing a random number of buffers, and picking one
|
2026-05-29 04:33:51 +02:00
|
|
|
|
* of the preconfigured »buffer types« (size) randomly. The corresponding
|
|
|
|
|
|
* BuffHandle(s) are passed through a queue; the head element from that queue
|
|
|
|
|
|
* is used to generate a combined CPU and memory load in each step.
|
|
|
|
|
|
* Deliberately, the pre-announced capacity does not exactly fit the generated
|
|
|
|
|
|
* load, so that constant rebalancing between local and global pool is provoked.
|
2026-05-29 23:46:20 +02:00
|
|
|
|
* @remark this test attempts to be _nasty_ — because the goal is to
|
|
|
|
|
|
* demonstrate correct behaviour under pressure.
|
2026-05-29 04:33:51 +02:00
|
|
|
|
*/
|
2026-05-28 19:15:03 +02:00
|
|
|
|
void
|
|
|
|
|
|
jobActivity()
|
|
|
|
|
|
{
|
2026-05-29 04:33:51 +02:00
|
|
|
|
lib::IterQueue<BuffHandle> buffers;
|
|
|
|
|
|
|
|
|
|
|
|
auto randomAlloc = [&]{
|
|
|
|
|
|
uint t = rand_.i (CONF::LOAD_SPREAD);
|
|
|
|
|
|
uint c = rand_.i (CONF::STEP_SPREAD);
|
|
|
|
|
|
for ( ; c>0; --c)
|
|
|
|
|
|
buffers.feed (Ctx::buffType_[t].lockBuffer());
|
|
|
|
|
|
};
|
|
|
|
|
|
auto randomFree = [&]{
|
|
|
|
|
|
uint c = rand_.i (CONF::STEP_SPREAD);
|
|
|
|
|
|
for ( ; c>0; --c)
|
|
|
|
|
|
if (buffers)
|
|
|
|
|
|
{
|
|
|
|
|
|
buffers->release();
|
|
|
|
|
|
++buffers;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
auto randomWork = [&]{
|
|
|
|
|
|
ENSURE (buffers);
|
|
|
|
|
|
size_t availSpace = buffers->size() / sizeof(size_t);
|
|
|
|
|
|
uint scaleStep = availSpace / CONF::BASE_BUFFSIZ;
|
|
|
|
|
|
ENSURE (availSpace*sizeof(size_t) >= load_.requiredBuffSiz(scaleStep));
|
|
|
|
|
|
load_.externalBuff = & buffers->accessAs<size_t>();
|
|
|
|
|
|
|
|
|
|
|
|
// repeatedly add contents of buffer to produce computational load
|
|
|
|
|
|
load_.invoke (scaleStep);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 23:46:20 +02:00
|
|
|
|
//---Announce-some-random-capacity-beforehand------
|
2026-05-29 04:33:51 +02:00
|
|
|
|
for (BuffDescr& buffType : Ctx::buffType_)
|
2026-05-29 23:46:20 +02:00
|
|
|
|
buffType.announce (rand_.i (CONF::STEP_SPREAD));
|
2026-05-29 04:33:51 +02:00
|
|
|
|
|
|
|
|
|
|
//---Process-sequence-of-work-steps-----
|
|
|
|
|
|
for (uint i=0; i<CONF::JOB_STEPS; ++i)
|
|
|
|
|
|
{
|
|
|
|
|
|
do randomAlloc();
|
|
|
|
|
|
while (buffers.empty());
|
|
|
|
|
|
randomWork();
|
|
|
|
|
|
randomFree();
|
|
|
|
|
|
}
|
|
|
|
|
|
//---Free-up-remaining-buffers--
|
|
|
|
|
|
while (buffers)
|
|
|
|
|
|
{
|
|
|
|
|
|
buffers->release();
|
|
|
|
|
|
++buffers;
|
|
|
|
|
|
}
|
2026-05-28 19:15:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/** one-time static pre-configuration of buffer types, shared by all threads.
|
|
|
|
|
|
* @remark this emulates a similar behaviour of the Render Node graph, where
|
|
|
|
|
|
* embedded BuffDescr instances are established by the Builder for each node.
|
|
|
|
|
|
* Furthermore, ComputationalLoad is calibrated here, so that at scaleStep≡1
|
|
|
|
|
|
* the imposed delay turns out close to the configured timeBase of 1ms.
|
|
|
|
|
|
* As a special twist, this test will use the allocated buffer memory
|
|
|
|
|
|
* to perform the computation to generate the load -- similar to what
|
|
|
|
|
|
* happens with a real CPU based media computation in memory.
|
|
|
|
|
|
*/
|
2026-05-28 19:15:03 +02:00
|
|
|
|
void
|
|
|
|
|
|
registerBufferTypes()
|
|
|
|
|
|
{
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
auto isKnown = [](HashVal typeID)
|
|
|
|
|
|
{ // deliberately check the central metadata store
|
|
|
|
|
|
// without accessing the local store of the main thread,
|
|
|
|
|
|
// to avoid carrying stale information beyond test case boundaries
|
|
|
|
|
|
// (the thread constructor runs in the main thread)
|
|
|
|
|
|
Depend<EngineBufferMetadata> metaHub;
|
|
|
|
|
|
return metaHub().isKnown (typeID);
|
|
|
|
|
|
};
|
2026-05-28 19:15:03 +02:00
|
|
|
|
if (Ctx::buffType_.empty()
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
or not isKnown (Ctx::buffType_[0]))
|
2026-05-28 19:15:03 +02:00
|
|
|
|
{
|
|
|
|
|
|
Ctx::buffType_.clear();
|
|
|
|
|
|
Ctx::buffType_.populate_by(
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
BuffTypes::invoke(
|
|
|
|
|
|
[&, i=0]() mutable
|
|
|
|
|
|
{ // preconfigure »buffer types« with suitable size
|
|
|
|
|
|
// as required by the Mem-method of ComputationalLoad
|
|
|
|
|
|
size_t siz = load_.requiredBuffSiz (++i);
|
|
|
|
|
|
return Ctx::bufferProvider().getDescriptorFor (siz);
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Ctx::calibrated_ != load_.sizeBase)
|
|
|
|
|
|
{
|
|
|
|
|
|
load_.calibrate();
|
|
|
|
|
|
Ctx::calibrated_ = load_.sizeBase;
|
2026-05-28 19:15:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-29 04:33:51 +02:00
|
|
|
|
|
2026-05-28 19:15:03 +02:00
|
|
|
|
public:
|
2026-05-29 04:33:51 +02:00
|
|
|
|
DummyWorkerThread() ///< @note this constructor runs in the main thread
|
2026-05-28 19:15:03 +02:00
|
|
|
|
: Thread{"Load-Test-Worker"
|
2026-05-29 04:33:51 +02:00
|
|
|
|
,[this]{ workActivity(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
, rand_{chainedRandGen()}
|
2026-05-28 19:15:03 +02:00
|
|
|
|
{
|
2026-05-29 04:33:51 +02:00
|
|
|
|
REQUIRE (CONF::LOAD_SPREAD <= MAX_SCALE);
|
|
|
|
|
|
|
|
|
|
|
|
load_.timeBase = 1ms;
|
|
|
|
|
|
load_.sizeBase = CONF::BASE_BUFFSIZ;
|
|
|
|
|
|
load_.useAllocation = true;
|
2026-05-28 19:15:03 +02:00
|
|
|
|
registerBufferTypes();
|
2026-05-29 04:33:51 +02:00
|
|
|
|
|
|
|
|
|
|
SyncBarrier::sync(); // allow the workActivity() to commence now...
|
2026-05-28 19:15:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 23:46:20 +02:00
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
/**************************************************************************//**
|
|
|
|
|
|
* @test Integration test of the Render Engine buffer management subsystem.
|
|
|
|
|
|
* - this test uses a setup and wiring of components similar to the
|
|
|
|
|
|
* actual Render Engine, with thread-local memory pools for each
|
|
|
|
|
|
* worker, and a central buffer allocator and metadata hub.
|
|
|
|
|
|
* - a load pattern similar to the actual render workers is generated
|
|
|
|
|
|
* by the DummyWorkerThread instances, exhibiting a randomly fluctuating
|
|
|
|
|
|
* demand of buffer memory, while keeping the CPU(s) busy with a load
|
|
|
|
|
|
* of memory access and simple computations.
|
|
|
|
|
|
* - the prime goal is to demonstrate correctness, balancing memory demand
|
|
|
|
|
|
* mostly through lock-free queues, without loosing any allocation.
|
2026-05-28 02:18:06 +02:00
|
|
|
|
*/
|
2026-06-24 23:48:15 +02:00
|
|
|
|
class BufferProviderLoad_test : public Test
|
2026-05-28 02:18:06 +02:00
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
virtual void
|
2026-05-31 23:48:26 +02:00
|
|
|
|
run (Arg arg)
|
2026-05-28 02:18:06 +02:00
|
|
|
|
{
|
2026-05-29 04:33:51 +02:00
|
|
|
|
seedRand();
|
2026-05-31 23:48:26 +02:00
|
|
|
|
if ("quick" == firstTok (arg))
|
|
|
|
|
|
MANY_THREADS = NOT_SO_MANY;
|
2026-05-29 04:33:51 +02:00
|
|
|
|
|
2026-05-28 02:18:06 +02:00
|
|
|
|
demonstrate_EngineSetup();
|
|
|
|
|
|
verify_massiveOverload();
|
|
|
|
|
|
verify_controlledLoad();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** @test show a setup of the buffer manager that is
|
|
|
|
|
|
* structurally equivalent to the Render Engine.
|
|
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
demonstrate_EngineSetup()
|
|
|
|
|
|
{
|
2026-05-28 19:15:03 +02:00
|
|
|
|
Ctx mockEngine;
|
2026-05-28 02:18:06 +02:00
|
|
|
|
|
2026-05-28 19:15:03 +02:00
|
|
|
|
DummyWorkerThread worker;
|
2026-05-28 02:18:06 +02:00
|
|
|
|
|
2026-05-28 19:15:03 +02:00
|
|
|
|
while (worker)
|
|
|
|
|
|
sleep_for (200us);
|
2026-05-28 02:18:06 +02:00
|
|
|
|
|
2026-05-29 04:33:51 +02:00
|
|
|
|
// the central services were indeed requested / created
|
|
|
|
|
|
CHECK (mockEngine.globalPool);
|
2026-05-29 23:46:20 +02:00
|
|
|
|
CHECK (mockEngine.metaHub);
|
2026-05-29 04:33:51 +02:00
|
|
|
|
|
|
|
|
|
|
// 5 distinct buffer types registered
|
|
|
|
|
|
CHECK (5 == mockEngine.metaHub->cntEntries());
|
|
|
|
|
|
|
|
|
|
|
|
sleep_for (500us); // additional delay for the local pool's destructor
|
|
|
|
|
|
// (which is invoked after the thread has terminated)
|
|
|
|
|
|
|
|
|
|
|
|
// there must be some allocations, still sitting in the return queue
|
|
|
|
|
|
CHECK (0 < watch(*mockEngine.globalPool).numAllocs());
|
|
|
|
|
|
|
|
|
|
|
|
// process and close remaining allocations
|
|
|
|
|
|
// sent back by the local pool's destructor...
|
|
|
|
|
|
mockEngine.globalPool->processPendingRequests();
|
|
|
|
|
|
// now there should be no memory leaks.......
|
|
|
|
|
|
CHECK (0 == watch(*mockEngine.globalPool).bytesLeased());
|
2026-05-28 19:15:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 02:18:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 23:46:20 +02:00
|
|
|
|
/** @test press the same setup using a
|
|
|
|
|
|
* massive overload of concurrent workers
|
2026-05-28 02:18:06 +02:00
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
verify_massiveOverload()
|
|
|
|
|
|
{
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
Ctx mockEngine;
|
|
|
|
|
|
|
|
|
|
|
|
// Start a collection of workers....
|
|
|
|
|
|
using Threads = ScopedCollection<DummyWorkerThread<>>;
|
|
|
|
|
|
Threads threads{MANY_THREADS, Threads::fill()};
|
|
|
|
|
|
|
|
|
|
|
|
// wait for all threads to finish...
|
|
|
|
|
|
while (explore(threads).has_any())
|
|
|
|
|
|
sleep_for (5ms);
|
|
|
|
|
|
|
2026-05-29 23:46:20 +02:00
|
|
|
|
CHECK (mockEngine.globalPool);
|
|
|
|
|
|
// again 5 distinct buffer types registered
|
|
|
|
|
|
CHECK (5 == mockEngine.metaHub->cntEntries());
|
|
|
|
|
|
CHECK (0 < watch(*mockEngine.globalPool).numAllocs());
|
BufferProvider: perform a successful stress-test
Yet only in a simple way, by pressing with 100 threads....
This test runs for about 10 seconds, shows a massive allocation
of more than 1 GiB, but comes down clean, without deadlocks,
using all cores and returning all memory blocks at the end.
This test uncovered some missing safety checks,
and it highlighted (again) that this memory management scheme is
elaborate and potentially fragile: I had to deal with carried-on
state in the local metadata registry associated with the main thread
(in fact I did not ever want that registry to be used, but turns out
that the initialisation check, that runs in the constructor, already
pulls up an instance for the main thread, which carries stale state
after the test case is complete)
2026-05-29 18:45:01 +02:00
|
|
|
|
|
|
|
|
|
|
sleep_for (500us);
|
|
|
|
|
|
mockEngine.globalPool->processPendingRequests();
|
2026-05-29 23:46:20 +02:00
|
|
|
|
CHECK (0 == watch(*mockEngine.globalPool).bytesLeased());
|
2026-05-28 02:18:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 23:46:20 +02:00
|
|
|
|
/** worker configuration for seamless full load */
|
|
|
|
|
|
struct FullLoadSetup : WorkSetup
|
|
|
|
|
|
{
|
|
|
|
|
|
static constexpr uint JOB_STEPS = 100; // number of processing steps in a single job
|
|
|
|
|
|
static constexpr uint STEP_SPREAD = 2; // reduced irregularity of allocations
|
|
|
|
|
|
static constexpr uint LOAD_SPREAD = 0; // effectively disable variation of buffer types
|
|
|
|
|
|
static constexpr uint MAX_DELAY = 0; // also disable random sleep between jobs
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** @test attempt to create a coordinated _full load_
|
|
|
|
|
|
* - configure the workers to compute without pause
|
|
|
|
|
|
* - use more repetitions in a single job
|
|
|
|
|
|
* - configured load time should be 1000ms per thread
|
|
|
|
|
|
* - use small number of workers; each gets its own core
|
|
|
|
|
|
* - capture the overall run time of the workers
|
|
|
|
|
|
* - coordination overhead should not be excessive
|
2026-05-28 02:18:06 +02:00
|
|
|
|
*/
|
|
|
|
|
|
void
|
|
|
|
|
|
verify_controlledLoad()
|
|
|
|
|
|
{
|
2026-05-29 23:46:20 +02:00
|
|
|
|
Ctx mockEngine;
|
|
|
|
|
|
using Threads = ScopedCollection<DummyWorkerThread<FullLoadSetup>>;
|
|
|
|
|
|
|
|
|
|
|
|
double time_us = benchmarkTime ([]{ // run a set of threads with full-load config...
|
|
|
|
|
|
Threads threads{FULL_LOAD_CORES, Threads::fill()};
|
|
|
|
|
|
while (explore(threads).has_any())
|
|
|
|
|
|
sleep_for (1ms);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
CHECK (time_us < 1.6e6);
|
|
|
|
|
|
sleep_for (500us);
|
|
|
|
|
|
mockEngine.globalPool->processPendingRequests();
|
|
|
|
|
|
CHECK (0 == watch(*mockEngine.globalPool).bytesLeased());
|
2026-05-28 02:18:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Register this test class... */
|
|
|
|
|
|
LAUNCHER (BufferProviderLoad_test, "unit engine");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}}} // namespace vault::mem::test
|