Skip to content

Commit

Permalink
Change: [Linkgraph] Pause the game when linkgraph jobs lag (OpenTTD#6470
Browse files Browse the repository at this point in the history
)

Check if the job is still running one date fract tick before it is due
to join and if so pause the game until its done.
This avoids the main thread being blocked on a thread join, which appears
to the user as if the game is unresponsive, as the UI does not repaint
and cannot be interacted with.
Show if pause is due to link graph job in status bar, update network
messages.
This does not apply for network clients.
  • Loading branch information
JGRennison committed Jan 22, 2019
1 parent c3dbe83 commit 2c8597f
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/lang/english.txt
Expand Up @@ -771,6 +771,7 @@ STR_SMALLMAP_TOOLTIP_ENABLE_ALL_CARGOS :{BLACK}Display
STR_STATUSBAR_TOOLTIP_SHOW_LAST_NEWS :{BLACK}Show last message or news report
STR_STATUSBAR_COMPANY_NAME :{SILVER}- - {COMPANY} - -
STR_STATUSBAR_PAUSED :{YELLOW}* * PAUSED * *
STR_STATUSBAR_PAUSED_LINK_GRAPH :{ORANGE}* * PAUSED (waiting for link graph update) * *
STR_STATUSBAR_AUTOSAVE :{RED}AUTOSAVE
STR_STATUSBAR_SAVING_GAME :{RED}* * SAVING GAME * *

Expand Down Expand Up @@ -2186,11 +2187,13 @@ STR_NETWORK_SERVER_MESSAGE_GAME_STILL_PAUSED_1 :Game still paus
STR_NETWORK_SERVER_MESSAGE_GAME_STILL_PAUSED_2 :Game still paused ({STRING}, {STRING})
STR_NETWORK_SERVER_MESSAGE_GAME_STILL_PAUSED_3 :Game still paused ({STRING}, {STRING}, {STRING})
STR_NETWORK_SERVER_MESSAGE_GAME_STILL_PAUSED_4 :Game still paused ({STRING}, {STRING}, {STRING}, {STRING})
STR_NETWORK_SERVER_MESSAGE_GAME_STILL_PAUSED_5 :Game still paused ({STRING}, {STRING}, {STRING}, {STRING}, {STRING})
STR_NETWORK_SERVER_MESSAGE_GAME_UNPAUSED :Game unpaused ({STRING})
STR_NETWORK_SERVER_MESSAGE_GAME_REASON_NOT_ENOUGH_PLAYERS :number of players
STR_NETWORK_SERVER_MESSAGE_GAME_REASON_CONNECTING_CLIENTS :connecting clients
STR_NETWORK_SERVER_MESSAGE_GAME_REASON_MANUAL :manual
STR_NETWORK_SERVER_MESSAGE_GAME_REASON_GAME_SCRIPT :game script
STR_NETWORK_SERVER_MESSAGE_GAME_REASON_LINK_GRAPH :waiting for link graph update
############ End of leave-in-this-order
STR_NETWORK_MESSAGE_CLIENT_LEAVING :leaving
STR_NETWORK_MESSAGE_CLIENT_JOINED :*** {RAW_STRING} has joined the game
Expand Down
3 changes: 2 additions & 1 deletion src/linkgraph/linkgraphjob.cpp
Expand Up @@ -40,7 +40,8 @@ LinkGraphJob::LinkGraphJob(const LinkGraph &orig) :
link_graph(orig),
settings(_settings_game.linkgraph),
thread(NULL),
join_date(_date + _settings_game.linkgraph.recalc_time)
join_date(_date + _settings_game.linkgraph.recalc_time),
job_completed(false)
{
}

Expand Down
13 changes: 11 additions & 2 deletions src/linkgraph/linkgraphjob.h
Expand Up @@ -15,6 +15,7 @@
#include "../thread/thread.h"
#include "linkgraph.h"
#include <list>
#include <atomic>

class LinkGraphJob;
class Path;
Expand Down Expand Up @@ -63,6 +64,7 @@ class LinkGraphJob : public LinkGraphJobPool::PoolItem<&_link_graph_job_pool>{
Date join_date; ///< Date when the job is to be joined.
NodeAnnotationVector nodes; ///< Extra node data necessary for link graph calculation.
EdgeAnnotationMatrix edges; ///< Extra edge data necessary for link graph calculation.
std::atomic<bool> job_completed; ///< Is the job still running. This is accessed by multiple threads and reads may be stale.

void EraseFlows(NodeID from);
void JoinThread();
Expand Down Expand Up @@ -267,18 +269,25 @@ class LinkGraphJob : public LinkGraphJobPool::PoolItem<&_link_graph_job_pool>{
* settings have to be brutally const-casted in order to populate them.
*/
LinkGraphJob() : settings(_settings_game.linkgraph), thread(NULL),
join_date(INVALID_DATE) {}
join_date(INVALID_DATE), job_completed(false) {}

LinkGraphJob(const LinkGraph &orig);
~LinkGraphJob();

void Init();

/**
* Check if job has actually finished.
* This is allowed to spuriously return an incorrect value.
* @return True if job has actually finished.
*/
inline bool IsJobCompleted() const { return this->job_completed.load(std::memory_order_acquire); }

/**
* Check if job is supposed to be finished.
* @return True if job should be finished by now, false if not.
*/
inline bool IsFinished() const { return this->join_date <= _date; }
inline bool IsScheduledToBeJoined() const { return this->join_date <= _date; }

/**
* Get the date when the job should be finished.
Expand Down
46 changes: 45 additions & 1 deletion src/linkgraph/linkgraphschedule.cpp
Expand Up @@ -16,6 +16,7 @@
#include "mcf.h"
#include "flowmapper.h"
#include "../framerate_type.h"
#include "../command_func.h"

#include "../safeguards.h"

Expand Down Expand Up @@ -50,14 +51,24 @@ void LinkGraphSchedule::SpawnNext()
}
}

/**
* Join the next finished job, if available.
*/
bool LinkGraphSchedule::IsJoinWithUnfinishedJobDue() const
{
if (this->running.empty()) return false;
const LinkGraphJob *next = this->running.front();
return next->IsScheduledToBeJoined() && !next->IsJobCompleted();
}

/**
* Join the next finished job, if available.
*/
void LinkGraphSchedule::JoinNext()
{
if (this->running.empty()) return;
LinkGraphJob *next = this->running.front();
if (!next->IsFinished()) return;
if (!next->IsScheduledToBeJoined()) return;
this->running.pop_front();
LinkGraphID id = next->LinkGraphIndex();
delete next; // implicitly joins the thread
Expand All @@ -79,6 +90,18 @@ void LinkGraphSchedule::JoinNext()
for (uint i = 0; i < lengthof(instance.handlers); ++i) {
instance.handlers[i]->Run(*job);
}

/*
* Readers of this variable in another thread may see an out of date value.
* However this is OK as this will only happen just as a job is completing,
* and the real synchronisation is provided by the thread join operation.
* In the worst case the main thread will be paused for longer than
* strictly necessary before joining.
* This is just a hint variable to avoid performing the join excessively
* early and blocking the main thread.
*/

job->job_completed.store(true, std::memory_order_release);
}

/**
Expand Down Expand Up @@ -141,6 +164,27 @@ LinkGraphSchedule::~LinkGraphSchedule()
}
}

/**
* Pause the game if on the next _date_fract tick, we would do a join with the next
* link graph job, but it is still running.
* If we previous paused, unpause if the job is now ready to be joined with
*/
void StateGameLoop_LinkGraphPauseControl()
{
if (_pause_mode & PM_PAUSED_LINK_GRAPH) {
/* We are paused waiting on a job, check the job every tick */
if (!LinkGraphSchedule::instance.IsJoinWithUnfinishedJobDue()) {
DoCommandP(0, PM_PAUSED_LINK_GRAPH, 0, CMD_PAUSE);
}
} else if (_pause_mode == PM_UNPAUSED &&
_date_fract == LinkGraphSchedule::SPAWN_JOIN_TICK - 1 &&
_date % _settings_game.linkgraph.recalc_interval == _settings_game.linkgraph.recalc_interval / 2 &&
LinkGraphSchedule::instance.IsJoinWithUnfinishedJobDue()) {
/* perform check one _date_fract tick before we would join */
DoCommandP(0, PM_PAUSED_LINK_GRAPH, 1, CMD_PAUSE);
}
}

/**
* Spawn or join a link graph job or compress a link graph if any link graph is
* due to do so.
Expand Down
3 changes: 3 additions & 0 deletions src/linkgraph/linkgraphschedule.h
Expand Up @@ -57,6 +57,7 @@ class LinkGraphSchedule {
static void Clear();

void SpawnNext();
bool IsJoinWithUnfinishedJobDue() const;
void JoinNext();
void SpawnAll();
void ShiftDates(int interval);
Expand All @@ -78,4 +79,6 @@ class LinkGraphSchedule {
void Unqueue(LinkGraph *lg) { this->schedule.remove(lg); }
};

void StateGameLoop_LinkGraphPauseControl();

#endif /* LINKGRAPHSCHEDULE_H */
1 change: 1 addition & 0 deletions src/misc_cmd.cpp
Expand Up @@ -152,6 +152,7 @@ CommandCost CmdPause(TileIndex tile, DoCommandFlag flags, uint32 p1, uint32 p2,
case PM_PAUSED_ERROR:
case PM_PAUSED_NORMAL:
case PM_PAUSED_GAME_SCRIPT:
case PM_PAUSED_LINK_GRAPH:
break;

#ifdef ENABLE_NETWORK
Expand Down
5 changes: 4 additions & 1 deletion src/network/network.cpp
Expand Up @@ -353,7 +353,8 @@ void NetworkHandlePauseChange(PauseMode prev_mode, PauseMode changed_mode)
case PM_PAUSED_NORMAL:
case PM_PAUSED_JOIN:
case PM_PAUSED_GAME_SCRIPT:
case PM_PAUSED_ACTIVE_CLIENTS: {
case PM_PAUSED_ACTIVE_CLIENTS:
case PM_PAUSED_LINK_GRAPH: {
bool changed = ((_pause_mode == PM_UNPAUSED) != (prev_mode == PM_UNPAUSED));
bool paused = (_pause_mode != PM_UNPAUSED);
if (!paused && !changed) return;
Expand All @@ -366,13 +367,15 @@ void NetworkHandlePauseChange(PauseMode prev_mode, PauseMode changed_mode)
if ((_pause_mode & PM_PAUSED_JOIN) != PM_UNPAUSED) SetDParam(++i, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_CONNECTING_CLIENTS);
if ((_pause_mode & PM_PAUSED_GAME_SCRIPT) != PM_UNPAUSED) SetDParam(++i, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_GAME_SCRIPT);
if ((_pause_mode & PM_PAUSED_ACTIVE_CLIENTS) != PM_UNPAUSED) SetDParam(++i, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_NOT_ENOUGH_PLAYERS);
if ((_pause_mode & PM_PAUSED_LINK_GRAPH) != PM_UNPAUSED) SetDParam(++i, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_LINK_GRAPH);
str = STR_NETWORK_SERVER_MESSAGE_GAME_STILL_PAUSED_1 + i;
} else {
switch (changed_mode) {
case PM_PAUSED_NORMAL: SetDParam(0, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_MANUAL); break;
case PM_PAUSED_JOIN: SetDParam(0, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_CONNECTING_CLIENTS); break;
case PM_PAUSED_GAME_SCRIPT: SetDParam(0, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_GAME_SCRIPT); break;
case PM_PAUSED_ACTIVE_CLIENTS: SetDParam(0, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_NOT_ENOUGH_PLAYERS); break;
case PM_PAUSED_LINK_GRAPH: SetDParam(0, STR_NETWORK_SERVER_MESSAGE_GAME_REASON_LINK_GRAPH); break;
default: NOT_REACHED();
}
str = paused ? STR_NETWORK_SERVER_MESSAGE_GAME_PAUSED : STR_NETWORK_SERVER_MESSAGE_GAME_UNPAUSED;
Expand Down
4 changes: 4 additions & 0 deletions src/openttd.cpp
Expand Up @@ -1349,6 +1349,10 @@ static void CheckCaches()
*/
void StateGameLoop()
{
if (!_networking || _network_server) {
StateGameLoop_LinkGraphPauseControl();
}

/* don't execute the state loop during pause */
if (_pause_mode != PM_UNPAUSED) {
PerformanceMeasurer::Paused(PFE_GAMELOOP);
Expand Down
1 change: 1 addition & 0 deletions src/openttd.h
Expand Up @@ -62,6 +62,7 @@ enum PauseMode {
PM_PAUSED_ERROR = 1 << 3, ///< A game paused because a (critical) error
PM_PAUSED_ACTIVE_CLIENTS = 1 << 4, ///< A game paused for 'min_active_clients'
PM_PAUSED_GAME_SCRIPT = 1 << 5, ///< A game paused by a game script
PM_PAUSED_LINK_GRAPH = 1 << 6, ///< A game paused due to the link graph schedule lagging

/** Pause mode bits when paused for network reasons. */
PMB_PAUSED_NETWORK = PM_PAUSED_ACTIVE_CLIENTS | PM_PAUSED_JOIN,
Expand Down
3 changes: 2 additions & 1 deletion src/statusbar_gui.cpp
Expand Up @@ -163,7 +163,8 @@ struct StatusBarWindow : Window {
} else if (_do_autosave) {
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_RIGHT, r.top + WD_FRAMERECT_TOP, STR_STATUSBAR_AUTOSAVE, TC_FROMSTRING, SA_HOR_CENTER);
} else if (_pause_mode != PM_UNPAUSED) {
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_RIGHT, r.top + WD_FRAMERECT_TOP, STR_STATUSBAR_PAUSED, TC_FROMSTRING, SA_HOR_CENTER);
StringID msg = (_pause_mode & PM_PAUSED_LINK_GRAPH) ? STR_STATUSBAR_PAUSED_LINK_GRAPH : STR_STATUSBAR_PAUSED;
DrawString(r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_RIGHT, r.top + WD_FRAMERECT_TOP, msg, TC_FROMSTRING, SA_HOR_CENTER);
} else if (this->ticker_scroll < TICKER_STOP && FindWindowById(WC_NEWS_WINDOW, 0) == NULL && _statusbar_news_item != NULL && _statusbar_news_item->string_id != 0) {
/* Draw the scrolling news text */
if (!DrawScrollingStatusText(_statusbar_news_item, this->ticker_scroll, r.left + WD_FRAMERECT_LEFT, r.right - WD_FRAMERECT_RIGHT, r.top + WD_FRAMERECT_TOP, r.bottom)) {
Expand Down

0 comments on commit 2c8597f

Please sign in to comment.