Files
DarkflameServer/dDashboardServer/routes/WorkerPool.h
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

96 lines
3.0 KiB
C++

#pragma once
#include <array>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <functional>
#include <mutex>
#include <optional>
#include <thread>
#include <vector>
/**
* A few worker threads for slow work that routes hand off with Web::Defer (converting the client's models), so the
* web server's one thread keeps answering. Jobs run by priority, in the order they came within one.
*
* The first thread is a fast lane that only runs URGENT jobs, so something small and wanted now (a flair near the
* camera) never waits behind big conversions filling the other threads. BACKGROUND jobs (converting ahead of time)
* only run when nothing else waits, and only a few at once, so they never take every thread.
*/
class WorkerPool {
public:
enum class ePriority : uint8_t {
URGENT, // small and wanted now: the fast lane takes these too
NORMAL,
LARGE, // big jobs someone waits for
BACKGROUND, // nobody waits for it
};
static constexpr size_t PRIORITIES = 4;
using Job = std::function<void()>;
WorkerPool() = default;
~WorkerPool() { Stop(); }
WorkerPool(const WorkerPool&) = delete;
WorkerPool& operator=(const WorkerPool&) = delete;
/**
* Start `threads` workers (at least 2: the fast lane and one more). At most `maxBackground` BACKGROUND jobs run
* at once (at least 1).
*/
void Start(size_t threads, size_t maxBackground = 1);
// Drop the queued jobs and wait for the running ones
void Stop();
bool Running() const { return !m_Threads.empty(); }
size_t Threads() const { return m_Threads.size(); }
/**
* Queue a job. `group` (0 for none) lets Cancel drop jobs queued together; `front` puts it before the others of
* its priority. Without threads (not started) the job runs right away on the caller's thread.
*/
void Submit(ePriority priority, Job job, uint64_t group = 0, bool front = false);
// Drop the queued jobs of `group`; returns how many
size_t Cancel(uint64_t group);
size_t Queued() const;
size_t Active() const;
// Wait until nothing is queued or running (for tests)
void WaitIdle();
/**
* Which priority a worker takes next from queues of these lengths: the most urgent one waiting, only URGENT for
* the fast lane, and BACKGROUND only while fewer than maxBackground of those run. nullopt: nothing for it.
*/
static std::optional<ePriority> Pick(const std::array<size_t, PRIORITIES>& queued, bool fastLane, size_t runningBackground, size_t maxBackground);
/**
* The default number of threads for this many CPU cores: half of them, from 2 to 4 (a conversion is one core's
* work, and the game servers on the same machine need the rest)
*/
static size_t DefaultThreads(size_t cores);
private:
struct Entry {
Job job;
uint64_t group{};
};
void Work(bool fastLane);
mutable std::mutex m_Mutex;
std::condition_variable m_Wake;
std::condition_variable m_Idle;
std::array<std::deque<Entry>, PRIORITIES> m_Queues;
std::vector<std::thread> m_Threads;
size_t m_MaxBackground{ 1 };
size_t m_RunningBackground{};
size_t m_Active{};
bool m_Stopping{};
};