diff --git a/src/vault/mem/buffer-provider.cpp b/src/vault/mem/buffer-provider.cpp index beae9241d..4cf7ff8d9 100644 --- a/src/vault/mem/buffer-provider.cpp +++ b/src/vault/mem/buffer-provider.cpp @@ -107,19 +107,15 @@ namespace mem { * various kinds of buffers (e.g. of differing size), which are * distinguished by _the type embodied into_ the BuffDescr. * @return maximum number of simultaneously usable buffers of this type, - * to be retrieved later through calls to #lockBuffer. - * @throw error::State when no buffer of this kind can be provided + * guaranteed to be retrieved later through calls to #lockBuffer. * @note the returned count may differ from the requested count. */ uint BufferProvider::announce (uint cnt, BuffDescr const& type) { size_t buffSiz = getBufferSize (type); - uint actually_possible = bufferStore_->prepareBuffers (type, cnt, buffSiz); - if (!actually_possible) - throw err::State ("unable to fulfil request for buffers" - ,LUMIERA_ERROR_BUFFER_MANAGEMENT); - return actually_possible; + uint immediately_possible = bufferStore_->prepareBuffers (type, cnt, buffSiz); + return immediately_possible; } diff --git a/tests/vault/gear/test-chain-load-test.cpp b/tests/vault/gear/test-chain-load-test.cpp index 8eac194cf..09ede91da 100644 --- a/tests/vault/gear/test-chain-load-test.cpp +++ b/tests/vault/gear/test-chain-load-test.cpp @@ -904,7 +904,7 @@ namespace test { CHECK (micros < 1100); micros = cpuLoad.invoke(5); CHECK (micros > 4600); - CHECK (micros > 5400); // usually quite precise, less than 1% off + CHECK (micros < 5400); // usually quite precise, less than 1% off micros = cpuLoad.invoke(10); CHECK (micros > 9500); micros = cpuLoad.invoke(100); diff --git a/tests/vault/gear/test-chain-load.hpp b/tests/vault/gear/test-chain-load.hpp index f851d31db..021e09a07 100644 --- a/tests/vault/gear/test-chain-load.hpp +++ b/tests/vault/gear/test-chain-load.hpp @@ -1390,9 +1390,6 @@ namespace test { { auto cnt = roundsNeeded(scaleStep); auto siz = max (scaleStep * sizeBase, 2u); - auto rep = max (cnt/siz, 1u); - // increase size to fit - siz = cnt / rep; return make_pair (siz,cnt); } @@ -1417,19 +1414,19 @@ namespace test { : memBlock.allocate (siz); Sink sink; *mem = sink+1; - for ( ; 0 < cnt; --cnt) + for (uint c=0; c < cnt; ++c) { - size_t i = cnt % (siz-1); + size_t i = c % (siz-1); mem[i+1] += mem[i]; } - sink = mem[siz-1]; + sink = mem[1 + (cnt-1) % (siz-1)]; sink = sink + 1; } double determineSpeed() { - uint step4gauge = 10; + uint step4gauge = 1; double micros = benchmark (step4gauge); auto stepsDone = roundsNeeded (step4gauge); return stepsDone / micros; diff --git a/tests/vault/mem/buffer-provider-load-test.cpp b/tests/vault/mem/buffer-provider-load-test.cpp index da491e83e..60c4c0ec4 100644 --- a/tests/vault/mem/buffer-provider-load-test.cpp +++ b/tests/vault/mem/buffer-provider-load-test.cpp @@ -26,23 +26,23 @@ #include "vault/gear/test-chain-load.hpp" #include "lib/scoped-collection.hpp" #include "lib/depend-inject.hpp" -#include "lib/iter-explorer.hpp" +//#include "lib/iter-explorer.hpp" #include "lib/sync-barrier.hpp" #include "lib/iter-stack.hpp" #include "lib/thread.hpp" -#include "lib/symbol.hpp" +//#include "lib/symbol.hpp" #include "lib/random.hpp" #include "test/diagnostic-output.hpp"////////////TODO -#include -#include +//#include +//#include #include -using lib::explore; +//using lib::explore; using lib::Depend; using lib::DependInject; using lib::Random; -using lib::Literal; +//using lib::Literal; using lib::Thread; using lib::SyncBarrier; using lib::ScopedCollection; @@ -57,19 +57,26 @@ namespace vault { namespace mem { namespace test { - namespace { // Test parameters...... - const size_t NUM_THREADS = 100; ///< press concurrently + namespace { // ========== Test Setup ========== - const size_t MAX_SCALE = 5; ///< maximum scale step for randomised resources + const size_t NUM_THREADS = 100; ///< press concurrently + const size_t MAX_SCALE = 5; ///< maximum scale step for randomised buffer types /** seed a per-thread random generator */ inline Random - makeRandGen() + chainedRandGen() { return Random{lib::seedFromDefaultGen()}; } + /** + * 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. + */ class MultithreadBufferSetup : public BufferProviderSetup { @@ -83,11 +90,18 @@ namespace test { }; + /** use a common selection of pre-configured buffer sizes */ using BuffTypes = ScopedCollection; - /** + /**************************************************//** * Mock dependency injection context * to emulate a Render Engine setup + * @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 */ struct Ctx { @@ -97,7 +111,6 @@ namespace test { // install mock instances for the relevant services DependInject::Local metaHub; DependInject::Local globalPool; - DependInject::Local frontEnd; }; // static storage for the mock-engine entrance point @@ -108,84 +121,174 @@ namespace test { + /* ============= Simulated Worker Load ============= */ + struct WorkSetup { - static constexpr uint NUM_JOBS = 10; - static constexpr size_t BASE_BUFFSIZ = 100_KiB; + 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 - /// local random generator per thread - Random rand_; - ComputationalLoad load_; - - WorkSetup() - : rand_{makeRandGen()} - { - load_.timeBase = 1ms; - load_.sizeBase = BASE_BUFFSIZ; - load_.useAllocation = true; - } + static constexpr size_t BASE_BUFFSIZ = 100_KiB; ///< @note scaled by sizeof(size_t) * LOAD_SPREAD }; /** - * Emulation of the load produced by a worker thread + * Simulation of the load produced by a worker thread. + * Performs a sequence of work actions, requesting + * buffer memory an causing computational load. */ template class DummyWorkerThread : CONF + , SyncBarrier , public Thread { + ComputationalLoad load_; + /// local random generator per thread + Random rand_; - + /**********//** + * Thread-Main + */ void workActivity() { + SyncBarrier::sync(); + for (uint i=0; i 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(); + + // repeatedly add contents of buffer to produce computational load + load_.invoke (scaleStep); + }; + + + //---Announce-guess-of-average-required-capacity----------- + for (BuffDescr& buffType : Ctx::buffType_) + buffType.announce (util::ceilDiv (CONF::STEP_SPREAD, 2u)); + + //---Process-sequence-of-work-steps----- + for (uint i=0; irelease(); + ++buffers; + } } + /** 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. + */ void registerBufferTypes() { if (Ctx::buffType_.empty() or Ctx::buffType_[0].buffSize() != CONF::BASE_BUFFSIZ ) { + load_.calibrate(); Ctx::buffType_.clear(); Ctx::buffType_.populate_by( BuffTypes::invoke( [&, i=0]() mutable - { - return Ctx::bufferProvider().getDescriptorFor (++i * CONF::BASE_BUFFSIZ); + { // 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); })); - } } + public: - DummyWorkerThread() + DummyWorkerThread() ///< @note this constructor runs in the main thread : Thread{"Load-Test-Worker" - ,[this]{ workActivity(); }} + ,[this]{ workActivity(); } + } + , rand_{chainedRandGen()} { + REQUIRE (CONF::LOAD_SPREAD <= MAX_SCALE); + + load_.timeBase = 1ms; + load_.sizeBase = CONF::BASE_BUFFSIZ; + load_.useAllocation = true; registerBufferTypes(); + + SyncBarrier::sync(); // allow the workActivity() to commence now... } }; - /*****************************************************************************//** - * @test Show how the central buffer manager of the Render Engine can be accessed. + + /**************************************************************************//** + * @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. */ class BufferProviderLoad_test : public Test { @@ -193,6 +296,8 @@ SHOW_EXPR(++ivo) virtual void run (Arg) { + seedRand(); + demonstrate_EngineSetup(); verify_massiveOverload(); verify_controlledLoad(); @@ -216,7 +321,27 @@ SHOW_EXPR(++ivo) SHOW_EXPR("hurgha") SHOW_EXPR(bool(mockEngine.globalPool)) SHOW_EXPR(bool(mockEngine.metaHub)) -SHOW_EXPR(bool(mockEngine.frontEnd)) + // the central services were indeed requested / created + CHECK (mockEngine.globalPool); + CHECK (mockEngine.globalPool); + +SHOW_EXPR(mockEngine.metaHub->cntEntries()) + // 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) + +SHOW_EXPR(watch(*mockEngine.globalPool).numAllocs()) + // 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....... +SHOW_EXPR(watch(*mockEngine.globalPool).bytesLeased()); + CHECK (0 == watch(*mockEngine.globalPool).bytesLeased()); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 71a448621..9f1dfdf7e 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -148357,7 +148357,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -148377,18 +148378,34 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + + - - - + + + + + + +

+ ...obwohl das mutmaßlich fast komplett genauso sein wird, wie für die echte Engine; jedoch wird damit das Test-Setup klarer verständlich, und ich kann ein lib::Depend-Singleton allein für diesen Test hier verwenden, ohne mir weiter Gedanken machen zu müssen +

+ +
+
+ + + + - + + @@ -148539,7 +148556,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + + @@ -148566,8 +148584,9 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + + @@ -148681,7 +148700,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -148695,23 +148715,20 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - - + + + - - + + - - + + - + - - - +

...das ist sogar wichtig, damit wir uns ähnlich wie in der echten Engine verhalten, incl. Zugriff über double-checked locking; @@ -148737,7 +148754,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + @@ -148751,15 +148769,24 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + + + + +

+ Stichwort Prüftiefe ⟹ ekelhaft sein +

+ +
+
- + @@ -148767,20 +148794,74 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + +

+ habe dazu die Mem-Methode so angepaßt, daß nun einfach direkt die konfigurierte sizeBase · scaleStep verwendet wird; da hatte ich bisher ziemlich (viel zu) raffinierten Code, da ich in der ersten Implementierung mit einer doppelt verschachtelten Schleife gearbeitet habe. Das war ungeschickt, und hat zu ungenauen Timings geführt. Mit dem Umbau hier nun verwenden wir einen Modulus, um die Load-Loop auf den Speicher herunterzubrechen; man kann nun einfach die konfigurierte Größe direkt verwenden. Damit kann ich relativ sicher einen vorhandenen Buffer verwenden (muß nur den scaleStep per Integer-Division herausfinden). +

+ +
+
+
+ + + + + + + + + + +

+ durch den effektiv ermittelten scaleStep zeigt sich, +

+

+ daß regelmäßig erheblich größere Buffer bereitgestellt werden. +

+

+ In einem Fall konnte ich sogar sehen, daß wohl der Pool geschrumpft +

+

+ worden war, denn eine Folge von Einzel-Anforderungen wurde stets +

+

+ mit dem viel zu großen Buffer bedient +

+ +
+ +
+ + +
+ + + + + + + + +
- + + +