blob: 4ccd788962f86336d29e26c2bc5dc9e637f39754 [file]
#include "catch.hpp"
#include "rive/async/job_pool.hpp"
#include <atomic>
#include <chrono>
#include <cmath>
using namespace rive;
namespace
{
class CountTask : public JobTask
{
public:
std::atomic<int> runs{0};
std::atomic<bool> interrupted{false};
std::thread::id ranOn;
protected:
void run() override
{
ranOn = std::this_thread::get_id();
runs++;
}
void interrupt() override { interrupted = true; }
};
// Runs until released, so a test can hold a pool thread.
class GateTask : public JobTask
{
public:
std::atomic<bool> release{false};
std::atomic<bool> started{false};
std::atomic<bool> interrupted{false};
protected:
void run() override
{
started = true;
while (!release.load() && !interrupted.load())
{
std::this_thread::yield();
}
}
void interrupt() override { interrupted = true; }
};
const JobSchedule kSchedules[] = {
JobSchedule::fifo,
JobSchedule::reverse,
JobSchedule::shuffle};
void waitStarted(GateTask* task)
{
while (!task->started.load())
{
std::this_thread::yield();
}
}
} // namespace
TEST_CASE("zero thread pool runs a joined task on the joiner", "[jobpool]")
{
JobPool pool(0);
auto task = make_rcp<CountTask>();
pool.submit(task);
CHECK(task->state() == JobTask::State::pending);
CHECK(pool.join(task.get(), 0));
CHECK(task->runs == 1);
CHECK(task->ranOn == std::this_thread::get_id());
CHECK(task->state() == JobTask::State::done);
}
TEST_CASE("zero thread pool runs queued tasks only when pumped", "[jobpool]")
{
JobPool pool(0);
auto a = make_rcp<CountTask>();
auto b = make_rcp<CountTask>();
pool.submit(a);
pool.submit(b);
CHECK(pool.hasQueued());
CHECK(pool.runPending(1) == 1);
CHECK(a->runs == 1);
CHECK(b->runs == 0);
CHECK(pool.runPending(8) == 1);
CHECK(b->runs == 1);
CHECK_FALSE(pool.hasQueued());
}
namespace
{
class SleepTask : public JobTask
{
public:
explicit SleepTask(double seconds) : m_seconds(seconds) {}
std::atomic<int> runs{0};
protected:
void run() override
{
runs++;
std::this_thread::sleep_for(std::chrono::duration<double>(m_seconds));
}
private:
double m_seconds;
};
} // namespace
TEST_CASE(
"a timed pump runs one task, then more within its seconds",
"[jobpool]")
{
JobPool pool(0);
auto slow = make_rcp<SleepTask>(0.05);
auto quick = make_rcp<CountTask>();
pool.submit(slow);
pool.submit(quick);
// The first runs to its end past the slice, which leaves no room.
CHECK(pool.runPendingFor(0.01) == 1);
CHECK(slow->runs == 1);
CHECK(quick->runs == 0);
auto another = make_rcp<CountTask>();
pool.submit(another);
CHECK(pool.runPendingFor(1) == 2);
CHECK(quick->runs == 1);
CHECK(another->runs == 1);
CHECK_FALSE(pool.hasQueued());
}
TEST_CASE("a join that may not run a task here leaves it queued", "[jobpool]")
{
JobPool pool(0);
auto task = make_rcp<CountTask>();
pool.submit(task);
CHECK_FALSE(pool.join(task.get(), 0, false));
JobTask* tasks[] = {task.get()};
CHECK(pool.joinAny(tasks, 0, false) == -1);
CHECK(task->runs == 0);
CHECK(task->state() == JobTask::State::pending);
CHECK(pool.runPending(8) == 1);
CHECK(task->runs == 1);
CHECK(task->state() == JobTask::State::done);
}
TEST_CASE("every task runs exactly once under every schedule", "[jobpool]")
{
for (JobSchedule schedule : kSchedules)
{
for (uint32_t threads : {0u, 1u, 4u})
{
JobPool pool(threads, schedule, 7);
std::vector<rcp<CountTask>> tasks;
for (int i = 0; i < 200; i++)
{
tasks.push_back(make_rcp<CountTask>());
pool.submit(tasks.back());
}
// Join in reverse so joiners race the pool threads for claims.
for (size_t i = tasks.size(); i-- > 0;)
{
REQUIRE(pool.join(tasks[i].get(), INFINITY));
}
for (auto& task : tasks)
{
REQUIRE(task->runs == 1);
}
}
}
}
TEST_CASE("join waits for a running task and honours its timeout", "[jobpool]")
{
JobPool pool(1);
auto gate = make_rcp<GateTask>();
pool.submit(gate);
waitStarted(gate.get());
auto start = std::chrono::steady_clock::now();
CHECK_FALSE(pool.join(gate.get(), 0.02));
CHECK(
std::chrono::steady_clock::now() - start >=
std::chrono::milliseconds(15));
gate->release = true;
CHECK(pool.join(gate.get(), INFINITY));
CHECK(gate->state() == JobTask::State::done);
}
TEST_CASE(
"joinAny returns a finished task or claims the lowest unstarted",
"[jobpool]")
{
JobPool pool(0);
auto a = make_rcp<CountTask>();
auto b = make_rcp<CountTask>();
pool.submit(a);
pool.submit(b);
JobTask* tasks[] = {a.get(), b.get()};
CHECK(pool.joinAny(tasks, 0) == 0);
CHECK(a->runs == 1);
CHECK(b->runs == 0);
// a has finished, so it answers again without running b.
CHECK(pool.joinAny(tasks, 0) == 0);
CHECK(b->runs == 0);
JobTask* onlyB[] = {b.get()};
CHECK(pool.joinAny(onlyB, 0) == 0);
CHECK(b->runs == 1);
}
TEST_CASE("joinAny times out while every task runs elsewhere", "[jobpool]")
{
JobPool pool(2);
auto a = make_rcp<GateTask>();
auto b = make_rcp<GateTask>();
pool.submit(a);
pool.submit(b);
waitStarted(a.get());
waitStarted(b.get());
JobTask* tasks[] = {a.get(), b.get()};
CHECK(pool.joinAny(tasks, 0.01) == -1);
b->release = true;
CHECK(pool.joinAny(tasks, INFINITY) == 1);
a->release = true;
CHECK(pool.join(a.get(), INFINITY));
}
TEST_CASE(
"a pool thread's reference goes before its joiner sees the task done",
"[jobpool]")
{
JobPool pool(4);
std::atomic<int> unfinished{0};
std::atomic<int> held{0};
// Each joiner also wakes for the others' tasks, so one soon catches a
// pool thread still holding a task it marked done.
std::vector<std::thread> joiners;
for (int i = 0; i < 4; i++)
{
joiners.emplace_back([&]() {
for (int round = 0; round < 5000; round++)
{
auto task = make_rcp<CountTask>();
pool.submit(task);
unfinished += !pool.join(task.get(), 10.0, false);
held += task->debugging_refcnt() != 1;
}
});
}
for (std::thread& joiner : joiners)
{
joiner.join();
}
CHECK(unfinished == 0);
CHECK(held == 0);
}
TEST_CASE("cancelling a pending task drops it", "[jobpool]")
{
JobPool pool(0);
auto task = make_rcp<CountTask>();
pool.submit(task);
pool.cancel(task.get());
CHECK(task->state() == JobTask::State::cancelled);
CHECK(task->cancelRequested());
CHECK(pool.join(task.get(), 0));
CHECK(pool.runPending(8) == 0);
CHECK(task->runs == 0);
}
TEST_CASE("cancelling a running task interrupts it", "[jobpool]")
{
JobPool pool(1);
auto gate = make_rcp<GateTask>();
pool.submit(gate);
waitStarted(gate.get());
pool.cancel(gate.get());
CHECK(pool.join(gate.get(), INFINITY));
CHECK(gate->interrupted);
}
TEST_CASE("the thread factory makes the pool threads", "[jobpool]")
{
std::atomic<int> made{0};
JobPool::setThreadFactory([&made](std::function<void()> loop) {
made++;
return std::thread(std::move(loop));
});
{
JobPool pool(3);
CHECK(pool.threadCount() == 3);
auto task = make_rcp<CountTask>();
pool.submit(task);
CHECK(pool.join(task.get(), INFINITY));
}
JobPool::setThreadFactory(nullptr);
CHECK(made == 3);
}
TEST_CASE("destroying a pool cancels what never started", "[jobpool]")
{
auto gate = make_rcp<GateTask>();
std::vector<rcp<CountTask>> queued;
{
JobPool pool(1);
pool.submit(gate);
waitStarted(gate.get());
for (int i = 0; i < 10; i++)
{
queued.push_back(make_rcp<CountTask>());
pool.submit(queued.back());
}
gate->release = true;
}
CHECK(gate->state() == JobTask::State::done);
for (auto& task : queued)
{
CHECK(
(task->state() == JobTask::State::cancelled ||
task->state() == JobTask::State::done));
CHECK(task->runs <= 1);
}
}
TEST_CASE(
"pool threads and joiners claim each task exactly once under load",
"[jobpool][stress]")
{
for (JobSchedule schedule : kSchedules)
{
JobPool pool(8, schedule, 11);
std::vector<rcp<CountTask>> tasks;
for (int round = 0; round < 50; round++)
{
size_t first = tasks.size();
for (int i = 0; i < 200; i++)
{
tasks.push_back(make_rcp<CountTask>());
pool.submit(tasks.back());
}
std::vector<JobTask*> batch;
for (size_t i = first; i < tasks.size(); i++)
{
batch.push_back(tasks[i].get());
}
// Drain with joinAny so the joiner claims while threads run.
size_t remaining = batch.size();
while (remaining > 0)
{
int index = pool.joinAny(batch, INFINITY);
REQUIRE(index >= 0);
batch.erase(batch.begin() + index);
remaining--;
}
}
for (auto& task : tasks)
{
REQUIRE(task->runs == 1);
}
}
}