Files
DarkflameServer/tests/dWebTests/WorkerPoolTests.cpp
Aaron Kimbrell 23e803280f feat: convert 3D scenery models on worker threads with deferred replies
The dashboard's web server answers one request at a time, so converting a big
.nif (glom files up to tens of MB) held up every other request, flairs included.

- dWeb: Web::Defer hands a request to another thread; the reply is sent from the
  web thread on its next poll (DeferredQueue). A client that leaves first cancels
  it and the late reply is dropped. The synchronous route API is unchanged.
- Web::Shutdown closes connections while the state their close events touch is
  still alive; the destructor no longer runs handlers during static destruction
  (stopping the dashboard aborted in ~WSClient).
- WorkerPool: priority lanes, with one thread only for urgent work (flairs,
  small models, textures), and limited background work.
- Scenery: mesh and texture routes (and the showcase's) convert on the pool;
  thread-safe memory and disk caches, one conversion per model at a time with
  waiters sharing it; zones are converted ahead onto the disk cache while viewed.
- Setting scenery_workers (0: half the cores, 2 to 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:48 -05:00

135 lines
4.8 KiB
C++

#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "WorkerPool.h"
using ePriority = WorkerPool::ePriority;
TEST(WorkerPoolTests, PickTakesTheMostUrgent) {
EXPECT_EQ(WorkerPool::Pick({ 0, 1, 1, 1 }, false, 0, 1), ePriority::NORMAL);
EXPECT_EQ(WorkerPool::Pick({ 2, 1, 1, 1 }, false, 0, 1), ePriority::URGENT);
EXPECT_EQ(WorkerPool::Pick({ 0, 0, 3, 1 }, false, 0, 1), ePriority::LARGE);
EXPECT_EQ(WorkerPool::Pick({ 0, 0, 0, 1 }, false, 0, 1), ePriority::BACKGROUND);
EXPECT_EQ(WorkerPool::Pick({ 0, 0, 0, 0 }, false, 0, 1), std::nullopt);
}
TEST(WorkerPoolTests, FastLaneOnlyTakesUrgentWork) {
EXPECT_EQ(WorkerPool::Pick({ 1, 1, 1, 1 }, true, 0, 1), ePriority::URGENT);
EXPECT_EQ(WorkerPool::Pick({ 0, 1, 1, 1 }, true, 0, 1), std::nullopt);
}
TEST(WorkerPoolTests, BackgroundWorkIsLimited) {
EXPECT_EQ(WorkerPool::Pick({ 0, 0, 0, 5 }, false, 1, 1), std::nullopt);
EXPECT_EQ(WorkerPool::Pick({ 0, 0, 0, 5 }, false, 1, 2), ePriority::BACKGROUND);
// Other work still goes ahead
EXPECT_EQ(WorkerPool::Pick({ 0, 1, 0, 5 }, false, 1, 1), ePriority::NORMAL);
}
TEST(WorkerPoolTests, DefaultThreads) {
EXPECT_EQ(WorkerPool::DefaultThreads(0), 2u);
EXPECT_EQ(WorkerPool::DefaultThreads(2), 2u);
EXPECT_EQ(WorkerPool::DefaultThreads(6), 3u);
EXPECT_EQ(WorkerPool::DefaultThreads(32), 4u);
}
TEST(WorkerPoolTests, WithoutThreadsJobsRunRightAway) {
WorkerPool pool;
bool ran = false;
pool.Submit(ePriority::NORMAL, [&ran] { ran = true; });
EXPECT_TRUE(ran);
}
TEST(WorkerPoolTests, RunsEveryJob) {
WorkerPool pool;
pool.Start(3);
std::atomic<int> count{};
for (int i = 0; i < 200; i++) pool.Submit(static_cast<ePriority>(i % 4), [&count] { count++; });
pool.WaitIdle();
EXPECT_EQ(count.load(), 200);
EXPECT_EQ(pool.Queued(), 0u);
}
// With every general thread busy on a big job, an urgent one still runs in the fast lane
TEST(WorkerPoolTests, UrgentWorkDoesNotWaitBehindBigJobs) {
WorkerPool pool;
pool.Start(2);
std::atomic<bool> release{}, bigStarted{}, urgentDone{};
pool.Submit(ePriority::LARGE, [&] {
bigStarted = true;
while (!release) std::this_thread::sleep_for(std::chrono::milliseconds(1));
});
while (!bigStarted) std::this_thread::sleep_for(std::chrono::milliseconds(1));
pool.Submit(ePriority::URGENT, [&] { urgentDone = true; });
for (int i = 0; i < 2000 && !urgentDone; i++) std::this_thread::sleep_for(std::chrono::milliseconds(1));
EXPECT_TRUE(urgentDone.load());
release = true;
pool.WaitIdle();
}
TEST(WorkerPoolTests, OrderWithinAndAcrossPriorities) {
WorkerPool pool;
pool.Start(2);
std::mutex mutex;
std::vector<std::string> order;
std::atomic<bool> release{}, blockerStarted{};
// Occupy the general thread so the rest queue up
pool.Submit(ePriority::NORMAL, [&] {
blockerStarted = true;
while (!release) std::this_thread::sleep_for(std::chrono::milliseconds(1));
});
while (!blockerStarted) std::this_thread::sleep_for(std::chrono::milliseconds(1));
const auto job = [&](std::string name) { return [&, name] { std::lock_guard lock(mutex); order.push_back(name); }; };
pool.Submit(ePriority::BACKGROUND, job("background"));
pool.Submit(ePriority::LARGE, job("large"));
pool.Submit(ePriority::NORMAL, job("normal 1"));
pool.Submit(ePriority::NORMAL, job("normal 2"));
pool.Submit(ePriority::NORMAL, job("normal 0"), 0, true);
release = true;
pool.WaitIdle();
EXPECT_EQ(order, (std::vector<std::string>{ "normal 0", "normal 1", "normal 2", "large", "background" }));
}
TEST(WorkerPoolTests, CancelDropsAGroupsQueuedJobs) {
WorkerPool pool;
pool.Start(2);
std::atomic<bool> release{}, blockerStarted{};
std::atomic<int> ran{};
pool.Submit(ePriority::NORMAL, [&] {
blockerStarted = true;
while (!release) std::this_thread::sleep_for(std::chrono::milliseconds(1));
});
while (!blockerStarted) std::this_thread::sleep_for(std::chrono::milliseconds(1));
for (int i = 0; i < 5; i++) pool.Submit(ePriority::BACKGROUND, [&ran] { ran++; }, 42);
pool.Submit(ePriority::BACKGROUND, [&ran] { ran += 100; }, 7);
EXPECT_EQ(pool.Cancel(42), 5u);
EXPECT_EQ(pool.Cancel(0), 0u);
release = true;
pool.WaitIdle();
EXPECT_EQ(ran.load(), 100);
}
TEST(WorkerPoolTests, StopDropsQueuedJobs) {
WorkerPool pool;
pool.Start(2);
std::atomic<bool> release{}, blockerStarted{};
std::atomic<int> ran{};
pool.Submit(ePriority::NORMAL, [&] {
blockerStarted = true;
while (!release) std::this_thread::sleep_for(std::chrono::milliseconds(1));
});
while (!blockerStarted) std::this_thread::sleep_for(std::chrono::milliseconds(1));
for (int i = 0; i < 5; i++) pool.Submit(ePriority::NORMAL, [&ran] { ran++; });
std::thread stopper([&pool] { pool.Stop(); });
std::this_thread::sleep_for(std::chrono::milliseconds(20));
release = true;
stopper.join();
EXPECT_EQ(ran.load(), 0);
EXPECT_FALSE(pool.Running());
}