LUMIERA.clone/tests/lib/local-slice-test.cpp
Ichthyostega 7448b81d49 Library: improve initial population of ScopedCollection
This is a collection with unique-ownership,
and the ability to create all content objects in ''RAII style''
from the constructor call, using a ''population functor''.

It is often used to maintain a collection of resource handles,
and especially to start a series of threads (for testing).

With the variadic λ-captures from C++20, it is possible to rewrite
the most frequently used population functors to be easier to use.
Notably we can now bind constructor arguments, that will be passed
to each of the created elements — a prominent example being the
thread function when launching several threads. Furthermore,
I added a constructor variant that consumes a Lumiera Iterator.
Adding this kind of generic variants is possible now with C++20,
since overload resolution takes concepts into account.
2026-05-14 22:46:06 +02:00

197 lines
6.1 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.

/*
LocalSlice(Test) - verify access to thread-local services
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 local-slice-test.cpp
** unit test \ref LocalSlice_test
*/
#include "test/run.hpp"
#include "test/test-helper.hpp"
#include "lib/scoped-collection.hpp"
#include "test/microbenchmark.hpp"
#include "lib/iter-explorer.hpp"
#include "lib/format-cout.hpp"
#include "lib/thread.hpp"
#include "lib/local-slice.hpp"
using lib::Thread;
using lib::explore;
using test::threadBenchmark;
using std::this_thread::yield;
using std::this_thread::sleep_for;
using std::chrono_literals::operator ""us;
namespace lib {
namespace test {
namespace { // test fixture...
const uint NUM_THREADS = 200;
constexpr size_t CONCURR_BENCH = 4;
const size_t REPETITIONS_BENCH = 1'000'000;
/**
* the "service" to be instantiated thread-local
*/
class DummyBackend
{
uint step{0};
public:
uint
getStep()
{
return ++step;
}
};
using DummySlice = LocalSlice<DummyBackend>;
/**
* A common shared service instance
* that dispatches through LocalSlice
* into the thread-local service back-ends
*/
class DummyFrontend
{
DummySlice dummySrv_;
public:
uint
invoke()
{
return dummySrv_->getStep()
+ dummySrv_->getStep()
+ dummySrv_->getStep()
+ dummySrv_->getStep()
;
}
};
//
} //(End) test fixture
/***********************************************************************//**
* @test demonstrate access to thread-local service instances.
* - setup a common front-end service instance
* - its functionality is implemented by delegating
* through the LocalSlice into the thread-local back-ends
* - launch threads and invoke the front-end concurrently
* @see local-slice.hpp
*/
class LocalSlice_test : public Test
{
virtual void
run (Arg)
{
simpleUsage();
investigatePerformance();
}
/** @test access thread-local service back-ends
* through a shared front-end instance;
* verify that each thread invokes a
* separate instance without interference
* @see ThreadWrapper_test
*/
void
simpleUsage ()
{
// Σi ≡ N/2·(N+1)
auto expectSum = [](uint n){ return n/2 * (n+1); };
// the front-end invokes the back-end four times in row...
uint i1 = expectSum(4);
uint i2 = expectSum(8) - expectSum(4);
uint i3 = expectSum(12) - expectSum(8);
// Note: all threads use this common service front-end instance
DummyFrontend myDummy;
// Now invoke through this front-end
CHECK (i1 == myDummy.invoke());
CHECK (i2 == myDummy.invoke());
// launch a significant number of threads,
// each verifying the same invocation sequence
using Threads = lib::ScopedCollection<Thread>;
Threads threads{NUM_THREADS
,Threads::fill ([&]{
CHECK (i1 == myDummy.invoke());
sleep_for (100us);
CHECK (i2 == myDummy.invoke());
sleep_for (100us);
CHECK (i3 == myDummy.invoke());
})};
while (explore(threads).has_any())
yield(); // wait for all threads to finish
// ...without any interference with the main thread's instance
CHECK (i3 == myDummy.invoke());
}
/** @test investigate performance of thread-local dispatch
* - subject-1 accesses the increment through the common thread-local dispatch
* - subject-2 increments just a local variable in each thread
* - run a multithreaded benchmark with 1 million repetitions
* - but only small number of threads to avoid OS scheduler congestion
* @see test::threadBenchmark()
*/
void
investigatePerformance()
{
DummySlice threadLocalBackend;
auto testThreadLocal = [&]{
auto volatile result = threadLocalBackend->getStep();
return result;
};
auto testLocalInc = [cnt=0] mutable
{
auto volatile result = ++cnt;
return result;
};
cout << "µ-benchmark :: thread-local" <<endl;
auto [t1, s1] = ::test::threadBenchmark<CONCURR_BENCH> (testThreadLocal, REPETITIONS_BENCH);
cout << "thread-local dispatch: "<<t1<<"µs" <<endl;
auto [t2, s2] = ::test::threadBenchmark<CONCURR_BENCH> (testLocalInc, REPETITIONS_BENCH);
cout << "increment local var : "<<t2<<"µs" <<endl;
CHECK (s1 == s2);
CHECK (t1 > t2);
}
};
/** Register this test class... */
LAUNCHER(LocalSlice_test, "unit common");
}} // namespace lib::test