Linkgraph: Changes to job scheduling algorithm.

This is to improve responsiveness of link graph updates, whilst
avoiding being blocked waiting for updates to complete.
Previously, large numbers of cheap jobs resulted in poor responsiveness
as it took a long time for jobs to cycle round.

Add 'linkgraph' debug category.
This commit is contained in:
Jonathan G Rennison
2016-10-31 00:21:01 +00:00
parent 7f10d83a4a
commit 84e61b690a
7 changed files with 82 additions and 24 deletions

View File

@@ -46,6 +46,7 @@ int _debug_gamelog_level;
int _debug_desync_level; int _debug_desync_level;
int _debug_yapfdesync_level; int _debug_yapfdesync_level;
int _debug_console_level; int _debug_console_level;
int _debug_linkgraph_level;
#ifdef RANDOM_DEBUG #ifdef RANDOM_DEBUG
int _debug_random_level; int _debug_random_level;
#endif #endif
@@ -75,6 +76,7 @@ struct DebugLevel {
DEBUG_LEVEL(desync), DEBUG_LEVEL(desync),
DEBUG_LEVEL(yapfdesync), DEBUG_LEVEL(yapfdesync),
DEBUG_LEVEL(console), DEBUG_LEVEL(console),
DEBUG_LEVEL(linkgraph),
#ifdef RANDOM_DEBUG #ifdef RANDOM_DEBUG
DEBUG_LEVEL(random), DEBUG_LEVEL(random),
#endif #endif

View File

@@ -54,6 +54,7 @@
extern int _debug_desync_level; extern int _debug_desync_level;
extern int _debug_yapfdesync_level; extern int _debug_yapfdesync_level;
extern int _debug_console_level; extern int _debug_console_level;
extern int _debug_linkgraph_level;
#ifdef RANDOM_DEBUG #ifdef RANDOM_DEBUG
extern int _debug_random_level; extern int _debug_random_level;
#endif #endif

View File

@@ -15,6 +15,7 @@
#include "../core/pool_type.hpp" #include "../core/pool_type.hpp"
#include "../core/smallmap_type.hpp" #include "../core/smallmap_type.hpp"
#include "../core/smallmatrix_type.hpp" #include "../core/smallmatrix_type.hpp"
#include "../core/bitmath_func.hpp"
#include "../station_base.h" #include "../station_base.h"
#include "../cargotype.h" #include "../cargotype.h"
#include "../date_func.h" #include "../date_func.h"
@@ -523,6 +524,11 @@ public:
NodeID AddNode(const Station *st); NodeID AddNode(const Station *st);
void RemoveNode(NodeID id); void RemoveNode(NodeID id);
inline uint CalculateCostEstimate() const {
uint64_t size_squared = this->Size() * this->Size();
return size_squared * FindLastBit(size_squared * size_squared); // N^2 * 4log_2(N)
}
protected: protected:
friend class LinkGraph::ConstNode; friend class LinkGraph::ConstNode;
friend class LinkGraph::Node; friend class LinkGraph::Node;

View File

@@ -28,9 +28,9 @@ INSTANTIATE_POOL_METHODS(LinkGraphJob)
*/ */
/* static */ Path *Path::invalid_path = new Path(INVALID_NODE, true); /* static */ Path *Path::invalid_path = new Path(INVALID_NODE, true);
static DateTicks GetLinkGraphJobJoinDateTicks() static DateTicks GetLinkGraphJobJoinDateTicks(uint duration_multiplier)
{ {
DateTicks ticks = _settings_game.linkgraph.recalc_time * DAY_TICKS; DateTicks ticks = _settings_game.linkgraph.recalc_time * DAY_TICKS * duration_multiplier;
if (_settings_game.linkgraph.recalc_not_scaled_by_daylength) { if (_settings_game.linkgraph.recalc_not_scaled_by_daylength) {
ticks /= _settings_game.economy.day_length_factor; ticks /= _settings_game.economy.day_length_factor;
} }
@@ -43,13 +43,13 @@ static DateTicks GetLinkGraphJobJoinDateTicks()
* original. The job is immediately started. * original. The job is immediately started.
* @param orig Original LinkGraph to be copied. * @param orig Original LinkGraph to be copied.
*/ */
LinkGraphJob::LinkGraphJob(const LinkGraph &orig) : LinkGraphJob::LinkGraphJob(const LinkGraph &orig, uint duration_multiplier) :
/* Copying the link graph here also copies its index member. /* Copying the link graph here also copies its index member.
* This is on purpose. */ * This is on purpose. */
link_graph(orig), link_graph(orig),
settings(_settings_game.linkgraph), settings(_settings_game.linkgraph),
thread(NULL), thread(NULL),
join_date_ticks(GetLinkGraphJobJoinDateTicks()), join_date_ticks(GetLinkGraphJobJoinDateTicks(duration_multiplier)),
start_date_ticks((_date * DAY_TICKS) + _date_fract), start_date_ticks((_date * DAY_TICKS) + _date_fract),
job_completed(false) job_completed(false)
{ {

View File

@@ -272,7 +272,7 @@ public:
LinkGraphJob() : settings(_settings_game.linkgraph), thread(NULL), LinkGraphJob() : settings(_settings_game.linkgraph), thread(NULL),
join_date_ticks(INVALID_DATE), start_date_ticks(INVALID_DATE), job_completed(false) {} join_date_ticks(INVALID_DATE), start_date_ticks(INVALID_DATE), job_completed(false) {}
LinkGraphJob(const LinkGraph &orig); LinkGraphJob(const LinkGraph &orig, uint duration_multiplier);
~LinkGraphJob(); ~LinkGraphJob();
void Init(); void Init();

View File

@@ -16,6 +16,7 @@
#include "mcf.h" #include "mcf.h"
#include "flowmapper.h" #include "flowmapper.h"
#include "../command_func.h" #include "../command_func.h"
#include <algorithm>
#include "../safeguards.h" #include "../safeguards.h"
@@ -27,27 +28,74 @@
/* static */ LinkGraphSchedule LinkGraphSchedule::instance; /* static */ LinkGraphSchedule LinkGraphSchedule::instance;
/** /**
* Start the next job in the schedule. * Start the next job(s) in the schedule.
*
* The cost estimate of a link graph job is C ~ N^2 log N, where
* N is the number of nodes in the job link graph.
* The cost estimate is summed for all running and scheduled jobs to form the total cost estimate T = sum C.
* The nominal cycle time (in recalc intervals) required to schedule all jobs is calculated as S = log_2 T.
* Hence the nominal duration of an individual job (in recalc intervals) is D = ceil(S * C / T)
* The cost budget for an individual call to this method is given by T / S.
*
* The purpose of this algorithm is so that overall responsiveness is not hindered by large numbers of small/cheap
* jobs which would previously need to be cycled through individually, but equally large/slow jobs have an extended
* duration in which to execute, to avoid unnecessary pauses.
*/ */
void LinkGraphSchedule::SpawnNext() void LinkGraphSchedule::SpawnNext()
{ {
if (this->schedule.empty()) return; if (this->schedule.empty()) return;
LinkGraph *next = this->schedule.front();
LinkGraph *first = next; GraphList schedule_to_back;
while (next->Size() < 2) { uint total_cost = 0;
this->schedule.splice(this->schedule.end(), this->schedule, this->schedule.begin()); for (auto iter = this->schedule.begin(); iter != this->schedule.end();) {
next = this->schedule.front(); auto current = iter;
if (next == first) return; ++iter;
const LinkGraph *lg = *current;
if (lg->Size() < 2) {
schedule_to_back.splice(schedule_to_back.end(), this->schedule, current);
} else {
total_cost += lg->CalculateCostEstimate();
}
} }
assert(next == LinkGraph::Get(next->index)); for (auto &it : this->running) {
this->schedule.pop_front(); total_cost += it->Graph().CalculateCostEstimate();
if (LinkGraphJob::CanAllocateItem()) {
LinkGraphJob *job = new LinkGraphJob(*next);
job->SpawnThread();
this->running.push_back(job);
} else {
NOT_REACHED();
} }
uint scaling = FindLastBit(total_cost);
uint cost_budget = total_cost / scaling;
uint used_budget = 0;
while (used_budget < cost_budget && !this->schedule.empty()) {
LinkGraph *lg = this->schedule.front();
assert(lg == LinkGraph::Get(lg->index));
this->schedule.pop_front();
uint cost = lg->CalculateCostEstimate();
used_budget += cost;
if (LinkGraphJob::CanAllocateItem()) {
uint duration_multiplier = CeilDiv(scaling * cost, total_cost);
std::unique_ptr<LinkGraphJob> job(new LinkGraphJob(*lg, duration_multiplier));
job->SpawnThread(); // todo
if (this->running.empty() || job->JoinDateTicks() >= this->running.back()->JoinDateTicks()) {
this->running.push_back(std::move(job));
DEBUG(linkgraph, 3, "LinkGraphSchedule::SpawnNext(): Running job: id: %u, nodes: %u, cost: %u, duration_multiplier: %u",
lg->index, lg->Size(), cost, duration_multiplier);
} else {
// find right place to insert
auto iter = std::upper_bound(this->running.begin(), this->running.end(), job->JoinDateTicks(), [](DateTicks a, const std::unique_ptr<LinkGraphJob> &b) {
return a < b->JoinDateTicks();
});
this->running.insert(iter, std::move(job));
DEBUG(linkgraph, 3, "LinkGraphSchedule::SpawnNext(): Running job (re-ordering): id: %u, nodes: %u, cost: %u, duration_multiplier: %u",
lg->index, lg->Size(), cost, duration_multiplier);
}
} else {
NOT_REACHED();
}
}
this->schedule.splice(this->schedule.end(), schedule_to_back);
DEBUG(linkgraph, 2, "LinkGraphSchedule::SpawnNext(): Linkgraph job totals: cost: %u, budget: %u, scaling: %u, scheduled: %zu, running: %zu",
total_cost, cost_budget, scaling, this->schedule.size(), this->running.size());
} }
/** /**
@@ -74,11 +122,11 @@ bool LinkGraphSchedule::IsJoinWithUnfinishedJobDue() const
void LinkGraphSchedule::JoinNext() void LinkGraphSchedule::JoinNext()
{ {
while (!(this->running.empty())) { while (!(this->running.empty())) {
LinkGraphJob *next = this->running.front(); if (!this->running.front()->IsFinished()) return;
if (!next->IsFinished()) return; std::unique_ptr<LinkGraphJob> next = std::move(this->running.front());
this->running.pop_front(); this->running.pop_front();
LinkGraphID id = next->LinkGraphIndex(); LinkGraphID id = next->LinkGraphIndex();
delete next; // implicitly joins the thread next.reset(); // implicitly joins the thread
if (LinkGraph::IsValidID(id)) { if (LinkGraph::IsValidID(id)) {
LinkGraph *lg = LinkGraph::Get(id); LinkGraph *lg = LinkGraph::Get(id);
this->Unqueue(lg); // Unqueue to avoid double-queueing recycled IDs. this->Unqueue(lg); // Unqueue to avoid double-queueing recycled IDs.

View File

@@ -13,6 +13,7 @@
#define LINKGRAPHSCHEDULE_H #define LINKGRAPHSCHEDULE_H
#include "linkgraph.h" #include "linkgraph.h"
#include <memory>
class LinkGraphJob; class LinkGraphJob;
@@ -40,7 +41,7 @@ private:
LinkGraphSchedule(); LinkGraphSchedule();
~LinkGraphSchedule(); ~LinkGraphSchedule();
typedef std::list<LinkGraph *> GraphList; typedef std::list<LinkGraph *> GraphList;
typedef std::list<LinkGraphJob *> JobList; typedef std::list<std::unique_ptr<LinkGraphJob>> JobList;
friend const SaveLoad *GetLinkGraphScheduleDesc(); friend const SaveLoad *GetLinkGraphScheduleDesc();
protected: protected: