Skip to content

Commit 503bc6b

Browse files
authored
Use high resolution timer on Windows (#3478)
This PR fixes timing issues on Windows. Simulation ratios have been observed as low as 6.5% on Windows with an empty scene, and simulation rates often hover around 80% with busier scenes. The problem was caused by the loop rate stabilization strategy. By default the Windows kernel timing resolution is 15.6 ms, meaning that a normal sleep command can not wake before 15.6 ms. On older version of Windows, a system-wide command was used to modify the system timing resolution, meaning that if any process requested a higher timing resolution all timers would also have this resolution. Newer Windows updates have changed this behavior, and each high resolution timer must now use the CREATE_WAITABLE_TIMER_HIGH_RESOLUTION with the Win32 API function CreateWaitableTimerEx() to create a timer that has the higher resolution capability. This PR modifies the SimulationRunner class to use a high resolution timer instead of std::this_thread::sleep_until() when on Windows. --------- Signed-off-by: John Wason <wason@wasontech.com>
1 parent 763dcd2 commit 503bc6b

File tree

2 files changed

+89
-3
lines changed

2 files changed

+89
-3
lines changed

src/SimulationRunner.cc

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@
6767
#include "LevelManager.hh"
6868
#include "SdfGenerator.hh"
6969

70+
#ifdef _WIN32
71+
#ifndef NOMINMAX
72+
#define NOMINMAX
73+
#endif
74+
#ifndef WIN32_LEAN_AND_MEAN
75+
#define WIN32_LEAN_AND_MEAN
76+
#endif
77+
#include <windows.h>
78+
#endif
79+
7080
using namespace gz;
7181
using namespace sim;
7282

@@ -104,6 +114,34 @@ struct MaybeGilScopedRelease
104114

105115
}
106116

117+
#ifdef _WIN32
118+
namespace gz
119+
{
120+
namespace sim
121+
{
122+
inline namespace GZ_SIM_VERSION_NAMESPACE {
123+
// Utility class to store the windows HANDLE variable and close
124+
// the handle using RAII. This class also hides the HANDLE
125+
// type from the global header files.
126+
class SimulationRunnerWinHandleStorage
127+
{
128+
private: HANDLE handleStorage{NULL};
129+
130+
public: HANDLE handle() { return handleStorage; }
131+
132+
public: SimulationRunnerWinHandleStorage(HANDLE h) : handleStorage(h) {}
133+
134+
public: ~SimulationRunnerWinHandleStorage() {
135+
if (handleStorage != NULL)
136+
{
137+
CloseHandle(handleStorage);
138+
}
139+
}
140+
};
141+
}
142+
}
143+
}
144+
#endif
107145

108146
//////////////////////////////////////////////////
109147
SimulationRunner::SimulationRunner(const sdf::World &_world,
@@ -183,6 +221,16 @@ SimulationRunner::SimulationRunner(const sdf::World &_world,
183221
);
184222
this->currentInfo.simTime = this->simTimeEpoch;
185223

224+
#ifdef _WIN32
225+
HANDLE winPrecisionTimerHandle = CreateWaitableTimerExA(NULL, NULL,
226+
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
227+
if (winPrecisionTimerHandle != NULL)
228+
{
229+
winPrecisionTimer = std::make_unique<SimulationRunnerWinHandleStorage>(
230+
winPrecisionTimerHandle);
231+
}
232+
#endif
233+
186234
// World control
187235
transport::NodeOptions opts;
188236
std::string ns{"/world/" + this->worldName};
@@ -914,14 +962,45 @@ bool SimulationRunner::Run(const uint64_t _iterations)
914962
// larger than the typical OS + CPU C-state latency.
915963
constexpr auto kSpinThreshold = 200us;
916964

965+
auto now = std::chrono::steady_clock::now();
966+
917967
// If the scheduled update time is in the future...
918-
if (nextUpdateTime > std::chrono::steady_clock::now())
968+
if (nextUpdateTime > now)
919969
{
920970
// ...sleep until we are close to the target time.
921971
auto sleepTarget = nextUpdateTime - kSpinThreshold;
922-
if (sleepTarget > std::chrono::steady_clock::now())
972+
if (sleepTarget > now)
923973
{
974+
#ifndef _WIN32
924975
std::this_thread::sleep_until(sleepTarget);
976+
#else
977+
if (winPrecisionTimer)
978+
{
979+
auto sleepTargetDuration =
980+
std::chrono::duration_cast<std::chrono::microseconds>(
981+
sleepTarget - now);
982+
LARGE_INTEGER due_time;
983+
memset(&due_time, 0, sizeof(due_time));
984+
// Positive durations are absolute, while negative durations
985+
// are relative in 10 us intervals.
986+
// The absolute time uses the non-precision system clock so we
987+
// need to use relative time.
988+
due_time.QuadPart = -sleepTargetDuration.count() * 10;
989+
if (SetWaitableTimer(winPrecisionTimer->handle(), &due_time, 0,
990+
NULL, NULL, FALSE) != TRUE)
991+
{
992+
gzerr << "Could not SetWaitableTimer" << std::endl;
993+
}
994+
else
995+
{
996+
WaitForSingleObject(winPrecisionTimer->handle(), INFINITE);
997+
}
998+
}
999+
else
1000+
{
1001+
std::this_thread::sleep_until(sleepTarget);
1002+
}
1003+
#endif
9251004
}
9261005

9271006
// ...then busy-wait for the final moments for precision.
@@ -932,7 +1011,7 @@ bool SimulationRunner::Run(const uint64_t _iterations)
9321011
}
9331012

9341013
// Schedule the next update time.
935-
auto now = std::chrono::steady_clock::now();
1014+
now = std::chrono::steady_clock::now();
9361015
nextUpdateTime += this->updatePeriod;
9371016
if (nextUpdateTime < now)
9381017
{

src/SimulationRunner.hh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ namespace gz
6969
inline namespace GZ_SIM_VERSION_NAMESPACE {
7070
// Forward declarations.
7171
class SimulationRunnerPrivate;
72+
#ifdef _WIN32
73+
class SimulationRunnerWinHandleStorage;
74+
#endif
7275

7376
class GZ_SIM_VISIBLE SimulationRunner
7477
{
@@ -589,6 +592,10 @@ namespace gz
589592
/// initialization and should exit immediately. See
590593
/// `SetExitedWithErrors()`.
591594
private: bool exitedWithErrors{false};
595+
#ifdef _WIN32
596+
private: std::unique_ptr<SimulationRunnerWinHandleStorage>
597+
winPrecisionTimer;
598+
#endif
592599

593600
friend class LevelManager;
594601
};

0 commit comments

Comments
 (0)