BufferProvider: cover capacity anouncement in unit test

...need an elaborate test setup, with the test worker thread
and the main thread proceeding in lockstep, otherwise the
effect would not be visible. Furthermore, the diagnostic
access for LocalMemPool can be re-used here, thereby
mapping requests onto the pool for the current thread.
This commit is contained in:
Fischlurch 2026-05-24 18:44:32 +02:00
parent e32eec38dd
commit 0256ad8528
4 changed files with 188 additions and 19 deletions

View file

@ -53,6 +53,9 @@ namespace mem {
using util::_Fmt;
class LocalStoreDiagnostic;
/**
* Production-grade implementation of buffer allocation
@ -98,8 +101,8 @@ namespace mem {
}
};
LocalPool localReg_;
GlobalPool globalReg_;
LocalPool localPool_;
GlobalPool globalPool_;
public:
LocalBufferStore()
@ -118,9 +121,9 @@ namespace mem {
uint
prepareBuffers (HashVal, uint numBuffers, size_t sizRequest) override
{
uint cnt = localReg_->reserve(numBuffers, sizRequest);
uint cnt = localPool_->reserve(numBuffers, sizRequest);
for (uint n{cnt}; n < numBuffers; ++n)
globalReg_().async_requestAllocation (*localReg_, sizRequest);
globalPool_().async_requestAllocation (*localPool_, sizRequest);
// signal back the number of buffers reserved and thus immediately available
return cnt;
}
@ -142,16 +145,16 @@ namespace mem {
BuffAlloc
provideBuffer (HashVal, size_t sizRequest, LocalTag specifics, int64_t) override
{
if (not localReg_->canServe(sizRequest))
globalReg_().processPendingRequests();
if (not localPool_->canServe(sizRequest))
globalPool_().processPendingRequests();
// attempt to satisfy allocation request from the local pool...
Alloc alloc = localReg_->retrieve (sizRequest);
Alloc alloc = localPool_->retrieve (sizRequest);
auto& [storage,buffSiz] = alloc;
// possibly the request was not announced beforehand...?
if (not storage)
alloc = globalReg_().requestAllocation (sizRequest);
alloc = globalPool_().requestAllocation (sizRequest);
if (not storage)
throw err::State{_Fmt{"Unable to provide further %d bytes of buffer memory."} % sizRequest
,LUMIERA_ERROR_BUFFER_MANAGEMENT};
@ -181,12 +184,23 @@ namespace mem {
{
auto& [storage,buffSiz,specifics] = storageSlot;
REQUIRE (storage);
localReg_->reSupply (storage);
localPool_->reSupply (storage);
/////////////////////////////////////////////////////OOO trigger heuristic clean-up here
}
private:
/** diagnostic access for unit testing */
friend PoolDiagnostic watch (LocalBufferStore const&);
};
/** entrance point to inspection for test */
inline PoolDiagnostic
watch (LocalBufferStore const& lstor)
{
return PoolDiagnostic{*lstor.localPool_};
}
}} // namespace vault::mem
#endif /*VAULT_MEM_LOCAL_BUFFER_STORE_H*/

View file

@ -508,6 +508,19 @@ namespace mem {
,[](Block const& b){ return not b.used; });
}
size_t
cntResd()
{
return std::ranges::count_if (memPool_.blocks_
,[](Block const& b){ return b.resd; });
}
bool
canServe (size_t sizRequest)
{
return memPool_.canServe (sizRequest);
} // map to the pool's diagnostic API for uniform access
bool
isFree (Buff* mem)
{

View file

@ -27,6 +27,7 @@
#include "test/diagnostic-output.hpp"///////////////////////TODO
#include <algorithm>
#include <atomic>
using util::isSameAdr;
@ -97,8 +98,8 @@ namespace test {
lib::DependInject<EngineBufferManager>::Local<EngineBufferManager> globalPool;
// provide the *test subject*,
// which implements the BufferStage API
// and uses a thread-local metadata table internally
// which implements the BufferStore API
// and uses a thread-local allocation pool internally
LocalBufferStore localStore;
BufferProviderSetup::Store& storeAPI = localStore;
@ -184,11 +185,98 @@ namespace test {
/** @test
/** @test announcement of capacity demand is propagated eventually
* - test thread must proceed in lockstep to make effects observable
* - Step-1: test thread announces demand on the subject (`BufferStore` API)
* - Step-2: „someone“ processes pending requests in the EngineBufferManager
* - Step-3: test thread announces further demand that remains unprocessed
* - test thread then terminates, sends back all allocations
* - the pending request is ignored transparently
*/
void
verify_announce()
{
// »central BufferPool« (transient instance for this test)
lib::DependInject<EngineBufferManager>::Local<EngineBufferManager> globalPool;
// *test subject*
LocalBufferStore localStore;
BufferProviderSetup::Store& storeAPI = localStore;
const size_t BUFFSIZ = 1024;
const HashVal TYPEID = 12345;
// coordination of test steps
std::atomic_uint step{1};
Thread testWorker{[&] /* === Scenario: test subject announces capacity demand === */
{
// Step-1
uint avail = storeAPI.prepareBuffers(TYPEID, 1, BUFFSIZ);
CHECK (0 == avail);
CHECK (watch(localStore).isEmpty());
// wait for being serviced...
++step;
while (step < 3)
sleep_for(1ms);
// Step-3
CHECK (not watch(localStore).isEmpty());
CHECK (watch(localStore).canServe(BUFFSIZ));
CHECK (watch(localStore).cntFree() == 1);
CHECK (watch(localStore).size() == 1);
// Note: this block was supplied but the local pool does not know why,
// since reservations are only tracked on blocks already available
CHECK (watch(localStore).cntResd() == 0);
// Now, instead of using this buffer block,
// rather generate another capacity announcement...
avail = storeAPI.prepareBuffers(TYPEID, 2, BUFFSIZ);
CHECK (1 == avail);
CHECK (watch(localStore).cntResd() == 1);
CHECK (watch(localStore).cntFree() == 1);
CHECK (watch(localStore).size() == 1);
CHECK (watch(localStore).canServe(BUFFSIZ));
// Note: while a further request is "out there",
// only a single block was received up to now.
// LocalMemPool winds down automatically at end of thread
}};
/* === Main thread: observe effects in the global pool === */
// wait until the first announcement was made
while (step < 2)
sleep_for(1ms);
// Step-2
CHECK (watch(*globalPool).numAllocs() == 0);
// process the in-queue and service the allocation request...
globalPool->processPendingRequests();
CHECK (watch(*globalPool).numAllocs() == 1);
CHECK (watch(*globalPool).bytesLeased() >= BUFFSIZ);
// let the test thread proceed...
step = 3;
// wait until test thread has wound down
while (testWorker)
sleep_for(1ms);
// additional delay to be sure the destructor of the local pool has finished
sleep_for(1ms);
CHECK (watch(*globalPool).numAllocs() == 1);
globalPool->processPendingRequests();
CHECK (watch(*globalPool).bytesLeased() == 0);
}

View file

@ -145226,8 +145226,7 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
Man m&#252;&#223;te dazu also den AllocReceiver umformulieren und zudem auch noch die Requests im EngineBufferManager. Vermutlich l&#228;uft das auf einen speziellen smart-Handle-Typ hinaus. Dieser w&#252;rde eine Pr&#252;fsumme beinhalten, die dann auf der Empf&#228;ngerseite genauso im Speicher vorzufinden sein mu&#223;. Die Pr&#252;fsumme w&#252;rde auf einer Nonce plus der Ziel-Addresse beruhen, und k&#246;nnte damit so nur ein einziges Mal pro Programmlauf auftreten (modulo 64-bit Hash-Kollision). Die Sperr-Flag best&#252;nde dann einfach darin, die empf&#228;ngerseitige Pr&#252;fsumme auf Null zu setzen (kein Match, kein Zugriff). Diese empf&#228;ngerseitige Pr&#252;fsumme m&#252;&#223;te ein Atomic sein, und beim Zugriff w&#252;rde eine Lese-Barriere angewendet, beim &#196;ndern eine Schreibbarriere. Das w&#228;re v&#246;llig akzeptabel. Dar&#252;berhinaus g&#228;be es keine Contention (au&#223;er beim Neuerzeugen eines Objekts, weil dann die Nonce inkrementiert werden mu&#223;).
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<arrowlink COLOR="#effec1" DESTINATION="ID_86232371" ENDARROW="Default" ENDINCLINATION="590;70;" ID="Arrow_ID_1616330137" STARTARROW="None" STARTINCLINATION="89;231;"/>
<icon BUILTIN="yes"/>
</node>
@ -145245,8 +145244,8 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
</p>
</body>
</html></richcontent>
<linktarget COLOR="#3d7395" DESTINATION="ID_332907520" ENDARROW="Default" ENDINCLINATION="-1840;207;" ID="Arrow_ID_668417083" SOURCE="ID_1820292066" STARTARROW="None" STARTINCLINATION="-634;41;"/>
<linktarget COLOR="#3d7395" DESTINATION="ID_332907520" ENDARROW="Default" ENDINCLINATION="-1840;207;" ID="Arrow_ID_1142922633" SOURCE="ID_142165791" STARTARROW="None" STARTINCLINATION="-634;41;"/>
<linktarget COLOR="#3d7395" DESTINATION="ID_332907520" ENDARROW="Default" ENDINCLINATION="-1840;207;" ID="Arrow_ID_668417083" SOURCE="ID_1820292066" STARTARROW="None" STARTINCLINATION="-634;41;"/>
<icon BUILTIN="yes"/>
<node CREATED="1779302523581" ID="ID_272378166" MODIFIED="1779302536450" TEXT="wird inkorporiert"/>
<node CREATED="1779302591078" ID="ID_1400105754" MODIFIED="1779302599488" TEXT="initialisiert sich automatisch mit einer Nonce"/>
@ -145592,12 +145591,67 @@ StM_bind(Builder&lt;R1&gt; b1, Extension&lt;R1,R2&gt; extension)
</node>
</node>
</node>
<node CREATED="1778948944946" ID="ID_399276838" MODIFIED="1778948954589" TEXT="verify_announce">
<node CREATED="1778948971327" ID="ID_655345868" MODIFIED="1778948993943" TEXT="mu&#xdf; hier die Pr&#xfc;fung bei laufendem Worker-Thread machen (barrier)"/>
<node CREATED="1778948997109" ID="ID_1331981737" MODIFIED="1778949018089" TEXT="Announcment mu&#xdf; im Engine-Pool sichtbar sein"/>
<node COLOR="#338800" CREATED="1778948944946" ID="ID_399276838" MODIFIED="1779640869852" TEXT="verify_announce">
<icon BUILTIN="button_ok"/>
<node BACKGROUND_COLOR="#c8c0b6" COLOR="#435e98" CREATED="1778948997109" ID="ID_1331981737" MODIFIED="1779640861359" TEXT="Announcement mu&#xdf; im Engine-Pool sichtbar sein">
<icon BUILTIN="yes"/>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1778948971327" ID="ID_655345868" MODIFIED="1779640850047" TEXT="mu&#xdf; hier die Pr&#xfc;fung bei laufendem Worker-Thread machen">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
...denn nach dem Thread-Ende wird die Allokation automatisch zur&#252;ckgef&#252;hrt, so da&#223; sich der Effekt nicht mehr beobachten l&#228;&#223;t
</p>
</body>
</html></richcontent>
<icon BUILTIN="messagebox_warning"/>
</node>
<node CREATED="1779633633221" ID="ID_580266326" MODIFIED="1779633638449" TEXT="schwierig zu testen">
<node CREATED="1779633639193" ID="ID_1983957454" MODIFIED="1779633649944" TEXT="da nun ja die Queue erst mal nicht bedient wird"/>
<node CREATED="1779633660674" ID="ID_552359761" MODIFIED="1779633677035" TEXT="au&#xdf;erdem: nach dem Thread-Ende soll der Request ignoriert werden"/>
</node>
<node CREATED="1779633705814" ID="ID_1901638981" MODIFIED="1779640605148" TEXT="brauche koordinierten mehrstufigen Test">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
...wenn man das erst mal akzeptiert, ist es auch nicht so schlimm: man inkrementiert einen Atomic
</p>
</body>
</html>
</richcontent>
</node>
</node>
<node COLOR="#435e98" CREATED="1779640700775" ID="ID_333012616" MODIFIED="1779640867890" TEXT="nach Zustellung mu&#xdf; die Allokation im lokalen Pool frei verf&#xfc;gbar sein"/>
<node COLOR="#435e98" CREATED="1779640715451" ID="ID_1548631619" MODIFIED="1779640867886">
<richcontent TYPE="NODE"><html>
<head>
</head>
<body>
<p>
ein weiters Announcement,
</p>
<p>
das nicht mehr zugestellt wird
</p>
<p>
wird transparent ignoriert
</p>
</body>
</html>
</richcontent>
</node>
</node>
<node CREATED="1778949021417" ID="ID_1256480563" MODIFIED="1778968909103" TEXT="verify_allocate">
<node CREATED="1778969027419" ID="ID_1633706484" MODIFIED="1778969039237" TEXT="gemeint ist: eine direkte Allokation, ohne Ank&#xfc;ndigung"/>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1779640817389" ID="ID_223109084" MODIFIED="1779640846459" TEXT="auch hier mu&#xdf; Beobachtung bei noch laufendem Worker erfolgen">
<icon BUILTIN="messagebox_warning"/>
</node>
</node>
<node CREATED="1778968918381" ID="ID_752345967" MODIFIED="1778968922655" TEXT="verify_release">
<node CREATED="1778969042432" ID="ID_1262962638" MODIFIED="1778969052166" TEXT="hier sollte auch ein clean-up provoziert werden"/>