LUMIERA.clone/tests/vault/gear/work-force-test.cpp
Ichthyostega 73737f2aee Library/Application: consolidate Monitor implementation
After the fundamental switch from POSIX to the C++14 wrappers
the existing implementation of the Monitor can now be drastically condensed,
removing several layers of indirection. Moreover, all signatures
shall be changed to blend in with the names and patterns established
by the C++ standard.

This is Step-1 : consolidate the Implementation.

(to ensure correctness, the existing API towards application code was retained)
2023-10-15 02:41:41 +02:00

502 lines
15 KiB
C++

/*
WorkForce(Test) - verify worker thread service
Copyright (C) Lumiera.org
2023, Hermann Vosseler <Ichthyostega@web.de>
This program 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.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* *****************************************************/
/** @file work-force-test.cpp
** unit test \ref WorkForce_test
*/
#include "lib/test/run.hpp"
#include "vault/gear/work-force.hpp"
#include "lib/thread.hpp"
#include "lib/sync.hpp"
#include "lib/test/diagnostic-output.hpp"////////////TODO
#include <functional>
#include <thread>
#include <chrono>
#include <set>
using test::Test;
namespace vault{
namespace gear {
namespace test {
using std::this_thread::sleep_for;
using namespace std::chrono_literals;
using std::chrono::milliseconds;
using lib::Thread;
namespace {
using WorkFun = std::function<work::SIG_WorkFun>;
using FinalFun = std::function<work::SIG_FinalHook>;
/**
* Helper: setup a Worker-Pool configuration for the test.
* Derived from the default configuration, it allows to bind
* a lambda as work-functor and to tweak other parameters.
*/
template<class FUN>
auto
setup (FUN&& workFun)
{
struct Setup
: work::Config
{
WorkFun doWork;
FinalFun finalHook = [](bool){ /*NOP*/ };
milliseconds IDLE_WAIT = work::Config::IDLE_WAIT;
size_t DISMISS_CYCLES = work::Config::DISMISS_CYCLES;
Setup (FUN&& workFun)
: doWork{std::forward<FUN> (workFun)}
{ }
Setup&&
withFinalHook (FinalFun finalFun)
{
finalHook = move (finalFun);
return move(*this);
}
Setup&&
withSleepPeriod (std::chrono::milliseconds millis)
{
IDLE_WAIT = millis;
return move(*this);
}
Setup&&
dismissAfter (size_t cycles)
{
DISMISS_CYCLES = cycles;
return move(*this);
}
};
return Setup{std::forward<FUN> (workFun)};
}
}
/*************************************************************************//**
* @test WorkForce-Service: maintain a pool of active worker threads.
* @warning this test relies on empirical timings and can be brittle.
* @see SchedulerUsage_test
*/
class WorkForce_test : public Test
{
virtual void
run (Arg)
{
simpleUsage();
verify_pullWork();
verify_workerHalt();
verify_workerSleep();
verify_workerDismiss();
verify_finalHook();
verify_detectError();
verify_defaultPool();
verify_scalePool();
verify_countActive();
verify_dtor_blocks();
}
/** @test demonstrate simple worker pool usage
*/
void
simpleUsage()
{
atomic<uint> check{0};
WorkForce wof{setup ([&]{ ++check; return activity::PASS; })};
// ^^^ this is the doWork-λ
CHECK (0 == check);
wof.activate();
sleep_for(20ms);
CHECK (0 < check); // λ invoked in the worker threads
}
/** @test the given work-functor is invoked repeatedly, once activated.
*/
void
verify_pullWork()
{
atomic<uint> check{0};
WorkForce wof{setup ([&]{ ++check; return activity::PASS; })};
CHECK (0 == check);
wof.incScale();
sleep_for(20ms);
uint invocations = check;
CHECK (0 < invocations);
sleep_for(2ms);
CHECK (invocations < check);
invocations = check;
sleep_for(2ms);
CHECK (invocations < check);
wof.awaitShutdown();
invocations = check;
sleep_for(2ms);
CHECK (invocations == check);
}
/** @test can cause a worker to terminate by return-value from the work-functor
*/
void
verify_workerHalt()
{
atomic<uint> check{0};
atomic<activity::Proc> control{activity::PASS};
WorkForce wof{setup ([&]{ ++check; return activity::Proc(control); })};
wof.incScale();
sleep_for(1ms);
uint invocations = check;
CHECK (0 < invocations);
control = activity::HALT;
sleep_for(1ms);
invocations = check;
sleep_for(10ms);
CHECK (invocations == check);
}
/** @test a worker can be sent to sleep, throttling the poll frequency.
*/
void
verify_workerSleep()
{
atomic<uint> check{0};
WorkForce wof{setup ([&]{ ++check; return activity::WAIT; })
.withSleepPeriod (10ms)};
wof.incScale();
sleep_for(1ms);
CHECK (1 == check);
sleep_for(10us);
CHECK (1 == check);
sleep_for(12ms); // after waiting one sleep-period...
CHECK (2 == check); // ...functor invoked again
}
/** @test when a worker is sent into sleep-cycles for an extended time,
* the worker terminates itself.
*/
void
verify_workerDismiss()
{
atomic<uint> check{0};
WorkForce wof{setup ([&]{ ++check; return activity::WAIT; })
.withSleepPeriod (10ms)
.dismissAfter(5)};
wof.incScale();
sleep_for(1ms);
CHECK (1 == check);
sleep_for(12ms);
CHECK (2 == check); // after one wait cycle, one further invocation
sleep_for(100ms);
CHECK (5 == check); // only 5 invocations total...
CHECK (0 == wof.size()); // ...after that, the worker terminated
}
/** @test verify invocation of a thread-termination callback
*/
void
verify_finalHook()
{
atomic<uint> exited{0};
atomic<activity::Proc> control{activity::PASS};
WorkForce wof{setup([&]{ return activity::Proc(control); })
.withFinalHook([&](bool){ ++exited; })};
CHECK (0 == exited);
wof.activate();
sleep_for(10ms);
CHECK (wof.size() == work::Config::COMPUTATION_CAPACITY);
CHECK (0 == exited);
control = activity::HALT;
sleep_for(10ms);
CHECK (0 == wof.size());
CHECK (exited == work::Config::COMPUTATION_CAPACITY);
}
/** @test exceptions emanating from within the worker are catched
* and reported by setting the isFailure argument flag of
* the `finalHook` functor invoked at worker termination.
*/
void
verify_detectError()
{
atomic<uint> check{0};
atomic<uint> errors{0};
WorkForce wof{setup ([&]{
if (++check == 555)
throw error::State("evil");
return activity::PASS;
})
.withFinalHook([&](bool isFailure)
{
if (isFailure)
++errors;
})};
CHECK (0 == check);
CHECK (0 == errors);
wof.incScale();
wof.incScale();
wof.incScale();
sleep_for(10us);
CHECK (3 == wof.size());
CHECK (0 < check);
CHECK (0 == errors);
sleep_for(200ms); // wait for the programmed disaster
CHECK (2 == wof.size());
CHECK (1 == errors);
}
/** @test by default, the WorkForce is initially inactive;
* once activated, it scales up to the number of cores
* reported by the runtime system.
*/
void
verify_defaultPool()
{
atomic<uint> check{0};
WorkForce wof{setup ([&]{ ++check; return activity::PASS; })};
// after construction, the WorkForce is inactive
CHECK (0 == wof.size());
CHECK (0 == check);
wof.activate();
sleep_for(20ms);
CHECK (0 < check);
CHECK (wof.size() == work::Config::COMPUTATION_CAPACITY);
CHECK (work::Config::COMPUTATION_CAPACITY == std::thread::hardware_concurrency());
}
/** @test the number of (separate) workers can be scaled up,
* both stepwise and as fraction of full hardware concurrency
*/
void
verify_scalePool()
{
/** helper to count distinct thread-IDs */
class UniqueCnt
: public std::set<std::thread::id>
, public lib::Sync<>
{
public:
void
mark (std::thread::id const& tID)
{
Lock guard(this);
this->insert(tID);
}
operator size_t() const
{
Lock guard(this);
return this->size();
}
}
uniqueCnt;
WorkForce wof{setup ([&]{
uniqueCnt.mark(std::this_thread::get_id());
return activity::PASS;
})};
CHECK (0 == uniqueCnt);
CHECK (0 == wof.size());
wof.incScale();
sleep_for(1ms);
CHECK (1 == uniqueCnt);
CHECK (1 == wof.size());
wof.incScale();
sleep_for(1ms);
CHECK (2 == uniqueCnt);
CHECK (2 == wof.size());
auto fullCnt = work::Config::COMPUTATION_CAPACITY;
wof.activate (1.0);
sleep_for(5ms);
CHECK (fullCnt == uniqueCnt);
CHECK (fullCnt == wof.size());
wof.activate (2.0);
sleep_for(5ms);
CHECK (2*fullCnt == uniqueCnt);
CHECK (2*fullCnt == wof.size());
wof.awaitShutdown();
CHECK (0 == wof.size());
uniqueCnt.clear();
sleep_for(5ms);
CHECK (0 == uniqueCnt);
wof.activate (0.5);
sleep_for(5ms);
CHECK (fullCnt/2 == uniqueCnt);
CHECK (fullCnt/2 == wof.size());
}
/** @test dynamically determine count of currently active workers.
*/
void
verify_countActive()
{
atomic<uint> check{0};
WorkForce wof{setup ([&]{
++check;
if (check == 5'000 or check == 5'110)
return activity::HALT;
else
return activity::PASS;
})};
CHECK (0 == wof.size());
wof.incScale();
wof.incScale();
wof.incScale();
sleep_for(10us); // this may be fragile; must be sufficiently short
CHECK (3 == wof.size());
while (check < 6'000)
sleep_for(10ms); // .....sufficiently long to count way beyond 10'000
CHECK (check > 6'000);
CHECK (1 == wof.size());
}
/** @test verify that the WorkForce dtor waits for all active threads to disappear
* - use a work-functor which keeps all workers blocked
* - start the WorkForce within a separate thread
* - in this separate thread, cause the WorkForce destructor to be called
* - in the test main thread release the work-functor blocking
* - at this point, all workers return, detect shutdown and terminate
*/
void
verify_dtor_blocks()
{
atomic<bool> trapped{true};
auto blockingWork = [&]{
while (trapped)
/* spin */;
return activity::PASS;
};
atomic<bool> pool_scaled_up{false};
atomic<bool> shutdown_done{false};
Thread operate{"controller"
,[&] {
{// nested scope...
WorkForce wof{setup (blockingWork)};
wof.activate();
sleep_for (10ms);
CHECK (wof.size() == work::Config::COMPUTATION_CAPACITY);
pool_scaled_up = true;
} // WorkForce goes out of scope => dtor called
// when reaching this point, dtor has terminated
shutdown_done = true;
}};
CHECK (operate); // operate-thread is in running state
sleep_for(100ms);
CHECK (pool_scaled_up);
CHECK (not shutdown_done); // all workers are trapped in the work-functor
// thus the destructor can't dismantle the pool
trapped = false;
sleep_for(20ms);
CHECK (shutdown_done);
CHECK (not operate); // operate-thread has detached and terminated
}
};
/** Register this test class... */
LAUNCHER (WorkForce_test, "unit engine");
}}} // namespace vault::gear::test