LUMIERA.clone/tests/basics/call-queue-test.cpp
Ichthyostega 46fc900980 UI-Dispatch: get the multithreded test to work (#1098)
the (trivial) implementation turned out to be correct as written,
but it was (again) damn challenging to get the mulithreaded chaotic
test fixture and especially the lambda captures to work correct.
2017-08-07 05:19:58 +02:00

260 lines
7.5 KiB
C++

/*
CallQueue(Test) - verify queue based dispatch of bound function objects
Copyright (C) Lumiera.org
2017, 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 call-queue-test.cpp
** unit test \ref CallQueue_test
*/
#include "lib/test/run.hpp"
#include "lib/scoped-collection.hpp"
#include "backend/thread-wrapper.hpp"
#include "lib/sync.hpp"
#include "lib/util.hpp"
#include "lib/call-queue.hpp"
#include <functional>
#include <string>
namespace lib {
namespace test{
using lib::Sync;
using backend::ThreadJoinable;
using util::isnil;
using std::string;
using std::bind;
namespace { // test fixture
// --------random-stress-test------
uint const NUM_OF_THREADS = 50;
uint const MAX_RAND_INCMT = 200;
uint const MAX_RAND_STEPS = 500;
uint const MAX_RAND_DELAY = 1000;
// --------random-stress-test------
uint calc_sum = 0;
uint ctor_sum = 0;
uint dtor_sum = 0;
template<uint i>
struct Dummy
{
uint val_;
Dummy()
: val_(i)
{
ctor_sum += (val_+1);
}
~Dummy()
{
dtor_sum += val_;
}
int
operator++()
{
return ++val_;
}
};
template<uint i>
void
increment (Dummy<i>&& dummy) //NOTE: dummy is consumed here
{
calc_sum += ++dummy;
}
}//(End) test fixture
/**********************************************************************************//**
* @test verify a helper component for dispatching functors through a threadsafe queue.
* - simple usage
* - enqueue and dequeue several functors
* - multithreaded stress test
*
* @see lib::CallQueue
* @see gui::NotificationService usage example
* @see [DemoGuiRoundtrip](http://issues.lumiera.org/ticket/1099 "Ticket #1099")
*/
class CallQueue_test : public Test
{
virtual void
run (Arg)
{
verify_SimpleUse();
verify_Consistency();
verify_ThreadSafety();
}
void
verify_SimpleUse ()
{
CallQueue queue;
CHECK (isnil (queue));
int val = 2;
queue.feed ([&]() { val = -1; });
CHECK (1 == queue.size());
CHECK (val == 2);
queue.invoke();
CHECK (val == -1);
CHECK (0 == queue.size());
queue.invoke();
CHECK (0 == queue.size());
}
/**
* @test consistency of queue data handling
* - functors of various types can be mixed
* - entries are moved in and out of the queue
* - no copying whatsoever happens
* - each entry gets invoked
* - all entries are invoked in order
* - enqueuing and dequeuing can be interspersed
* - no memory leaks in bound argument data
*/
void
verify_Consistency ()
{
calc_sum = 0;
ctor_sum = 0;
dtor_sum = 0;
CallQueue queue;
queue.feed ([]() { increment(Dummy<0>{}); }); //NOTE: each lambda binds a different instantiation of the increment template
queue.feed ([]() { increment(Dummy<1>{}); }); // and each invocation closes over an anonymous rvalue instance
queue.feed ([]() { increment(Dummy<2>{}); });
queue.invoke();
queue.invoke();
queue.feed ([]() { increment(Dummy<3>{}); });
queue.feed ([]() { increment(Dummy<4>{}); });
queue.invoke();
queue.invoke();
queue.invoke();
uint expected = (5+1)*5/2;
CHECK (calc_sum = expected);
CHECK (ctor_sum = expected);
CHECK (dtor_sum = expected);
}
struct Worker
: ThreadJoinable
, Sync<>
{
uint64_t producerSum = 0;
uint64_t consumerSum = 0;
void
countConsumerCall (uint increment)
{
Lock sync(this); // NOTE: will be invoked from some random other thread
consumerSum += increment;
}
Worker(CallQueue& queue)
: ThreadJoinable{"CallQueue_test: concurrent dispatch"
, [&]() {
uint cnt = rand() % MAX_RAND_STEPS;
uint delay = rand() % MAX_RAND_DELAY;
for (uint i=0; i<cnt; ++i)
{
uint increment = rand() % MAX_RAND_INCMT;
queue.feed ([=]() { countConsumerCall(increment); });
producerSum += increment;
usleep (delay);
queue.invoke(); // NOTE: dequeue one functor added during our sleep
} // and thus belonging to some random other thread
}}
{ }
};
using Workers = lib::ScopedCollection<Worker>;
/**
* @test torture the CallQueue by massively multithreaded dispatch
* - start #NUM_OF_THREADS (e.g. 50) threads in parallel
* - each of those has a randomised execution pattern to
* add new functors and dispatch other thread's functors
*/
void
verify_ThreadSafety()
{
CallQueue queue;
// Start a bunch of threads with random access pattern
Workers workers{NUM_OF_THREADS,
[&](Workers::ElementHolder& storage)
{
storage.create<Worker>(queue);
}
};
// wait for termination of all threads
for (auto& worker : workers)
worker.join();
// collect the results of all worker threads
uint64_t globalProducerSum = 0;
uint64_t globalConsumerSum = 0;
for (auto& worker : workers)
{
globalProducerSum += worker.producerSum;
globalConsumerSum += worker.consumerSum;
}
// VERIFY: locally recorded partial sums match total sum
CHECK (globalProducerSum == globalConsumerSum);
}
};
/** Register this test class... */
LAUNCHER (CallQueue_test, "unit common");
}} // namespace lib::test