feat(time): implement timekeeping utilities (#1)

* feat(bc): implement timekeeping

* fix(time): Milliseconds and Seconds return storage should be 64-bit

* fix(time): nsec should be explicitly cast to uint32

* test(time): rudimentary tests for timekeeping

* fix(time): check properly for timestamp + nsec overflows in MakeTime and BreakTime

* fix(time): include UNIX time headers on Mac, too

* test(time): test can tolerate system hiccups
This commit is contained in:
phaneron 2023-08-03 17:15:10 -04:00 committed by GitHub
parent d31a66b9ca
commit 6b1ba4cdcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 567 additions and 1 deletions

View file

@ -1,7 +1,8 @@
file(GLOB BC_SOURCES
"*.cpp"
"lock/*.cpp"
"system/**.cpp"
"time/*.cpp"
"system/*.cpp"
)
add_library(bc STATIC

6
bc/Time.hpp Normal file
View file

@ -0,0 +1,6 @@
#ifndef BC_TIME_HPP
#define BC_TIME_HPP
#include "bc/time/Time.hpp"
#endif

177
bc/system/System_Time.cpp Normal file
View file

@ -0,0 +1,177 @@
#include "bc/system/System_Time.hpp"
#include "bc/time/Time.hpp"
#include "bc/time/TimeConst.hpp"
#if defined(WHOA_SYSTEM_MAC)
#include <mach/mach_time.h>
#endif
#if defined(WHOA_SYSTEM_WIN)
#include <windows.h>
#endif
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
#include <ctime>
#include <sys/time.h>
#endif
#include "bc/Debug.hpp"
namespace Blizzard {
namespace System_Time {
// Globals
// Stores the earliest TSC value
static uint64_t s_absBegin = 0;
// Stores the number of nanoseconds since Jan 1, 2000 00:00 GMT at the raw clock moment of s_absBegin.
static Time::Timestamp s_gmBegin = 0;
// timeScales can be multiplied against number of ticks since s_absBegin to get
// meaningful durations in the corresponding format
double timeScaleNanoseconds = 0.0;
double timeScaleMicroseconds = 0.0;
double timeScaleMilliseconds = 0.0;
double timeScaleSeconds = 0.0;
// Functions
bool ReadTSC(uint64_t& counter) {
#if defined(WHOA_SYSTEM_WIN)
LARGE_INTEGER li;
auto ok = QueryPerformanceCounter(&li);
if (ok) {
counter = static_cast<uint64_t>(li.QuadPart);
return true;
}
return false;
#elif defined(WHOA_SYSTEM_MAC)
counter = mach_absolute_time();
return true;
#elif defined(WHOA_SYSTEM_LINUX)
struct timespec ts;
auto status = clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
if (status == 0) {
counter = (static_cast<uint64_t>(ts.tv_sec) * TimeConst::TimestampsPerSecond) + ts.tv_nsec;
return true;
}
return false;
#endif
}
// Returns a clock moment, relative to s_absBegin;
uint64_t QueryClockMoment() {
uint64_t counter = 0;
ReadTSC(counter);
return counter - s_absBegin;
}
// Returns Y2K-GMT nanosecond time.
Time::Timestamp Now() {
CheckInit();
// Record clock moment
auto moment = QueryClockMoment();
// Add moment to GMT
return s_gmBegin + static_cast<Time::Timestamp>(timeScaleNanoseconds * static_cast<double>(moment));
}
// this func is run on first use
void TimeInit() {
// Record absolute clock beginning moment in raw CPU time
ReadTSC(s_absBegin);
BLIZZARD_ASSERT(s_absBegin != 0);
// Look at system clock's GMT/UTC time as nanoseconds
// This associates a point in GMT with the more precise measurements obtained from reading the timestamp counter
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
// Unix system clock
struct timeval tv;
gettimeofday(&tv, nullptr);
s_gmBegin = Time::FromUnixTime(tv.tv_sec) + (tv.tv_usec * 1000ULL);
#elif defined(WHOA_SYSTEM_WIN)
// Read Win32 system time
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
// Convert time into Blizzard timestamp
ULARGE_INTEGER ul = {};
ul.HighPart = ft.dwHighDateTime;
ul.LowPart = ft.dwLowDateTime;
s_gmBegin = Time::FromWinFiletime(ul.QuadPart);
#endif
// Attempt to figure out the scale of TSC durations in real-time
#if defined(WHOA_SYSTEM_WIN)
// Read frequency with Win32 API
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
auto ticksPerSecond = static_cast<uint64_t>(freq.QuadPart);
auto ticksPerNanosecond = static_cast<double>(ticksPerSecond) / static_cast<double>(TimeConst::TimestampsPerSecond);
timeScaleNanoseconds = 1.0 / ticksPerNanosecond;
#elif defined(WHOA_SYSTEM_MAC)
// Ask Mach what the base time parameters are
mach_timebase_info_data_t timebase;
mach_timebase_info(&timebase);
timeScaleNanoseconds = static_cast<double>(timebase.numer) / static_cast<double>(timebase.denom);
#elif defined(WHOA_SYSTEM_LINUX)
// clock_gettime is already attuned to timestamp counter frequency
timeScaleNanoseconds = 1.0;
#endif
timeScaleMicroseconds = timeScaleNanoseconds / 1000.0;
timeScaleMilliseconds = timeScaleNanoseconds / 1000000.0;
timeScaleSeconds = timeScaleNanoseconds / 1000000000.0;
}
void CheckInit() {
if (s_absBegin == 0) {
TimeInit();
}
}
// Wall clock functions. The values returned are of an arbitrary epoch.
// The only guarantee is that the values returned will increase monotonically.
// Get wall clock time in nanoseconds
uint64_t Nanoseconds() {
CheckInit();
uint64_t tsc;
ReadTSC(tsc);
return static_cast<uint64_t>(static_cast<double>(tsc) * timeScaleNanoseconds);
}
// Get wall clock time in microseconds
uint64_t Microseconds() {
CheckInit();
uint64_t tsc;
ReadTSC(tsc);
return static_cast<uint64_t>(static_cast<double>(tsc) * timeScaleMicroseconds);
}
// Get wall clock time in milliseconds
uint64_t Milliseconds() {
CheckInit();
uint64_t tsc;
ReadTSC(tsc);
return static_cast<uint64_t>(static_cast<double>(tsc) * timeScaleMilliseconds);
}
// Get wall clock time in seconds
uint64_t Seconds() {
CheckInit();
uint64_t tsc;
ReadTSC(tsc);
return static_cast<uint64_t>(static_cast<double>(tsc) * timeScaleSeconds);
}
} // namespace System_Time
} // namespace Blizzard

25
bc/system/System_Time.hpp Normal file
View file

@ -0,0 +1,25 @@
#ifndef BC_SYSTEM_TIME_HPP
#define BC_SYSTEM_TIME_HPP
#include <cstdint>
#include "bc/time/Types.hpp"
namespace Blizzard {
namespace System_Time {
Time::Timestamp Now();
void CheckInit();
uint64_t QueryClockMoment();
uint64_t Nanoseconds();
uint64_t Microseconds();
uint64_t Milliseconds();
uint64_t Seconds();
} // namespace System_Time
} // namespace Blizzard
#endif

230
bc/time/Time.cpp Normal file
View file

@ -0,0 +1,230 @@
#include "bc/time/Time.hpp"
#include "bc/time/TimeConst.hpp"
#include "bc/system/System_Time.hpp"
#if defined(WHOA_SYSTEM_WIN)
#include <windows.h>
#endif
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
#include <ctime>
#endif
#include <limits>
namespace Blizzard {
namespace Time {
// Global variables
// Jan = [1],
// Feb = [2] and so on
static uint32_t s_monthDays[14] = {
0x41F00000, // Invalid?
0,
31,
59,
90,
120,
151,
182,
212,
243,
273,
304,
334,
365
};
// Functions
// Convert Blizzard timestamp to UNIX seconds
int32_t ToUnixTime(Timestamp timestamp) {
// Can't return time prior to 1901
// Return minimum time (1901) in case of underflow
if (timestamp < -3094168447999999999LL) {
return std::numeric_limits<int32_t>::min();
}
// 32-bit UNIX sec time suffers from the Year 2038 problem
// Return maximum time (2038) in case of overflow
if (timestamp >= 1200798847000000000LL) {
return std::numeric_limits<int32_t>::max();
}
// Go back 30 years
auto y1970 = timestamp + 946684800000000000LL;
// Convert nanoseconds to seconds
return static_cast<uint32_t>(y1970 / TimeConst::TimestampsPerSecond);
}
// TODO: look into making this Y2038-aware.
// Convert UNIX seconds into Blizzard timestamp
Timestamp FromUnixTime(int32_t unixTime) {
// Convert seconds to nanoseconds
auto unixnano = int64_t(unixTime) * TimeConst::TimestampsPerSecond;
// Move forward 30 years
auto y2k = unixnano - 946684800000000000LL;
return static_cast<Timestamp>(y2k);
}
// Win32 FILETIME to y2k
Timestamp FromWinFiletime(uint64_t winTime) {
if (winTime < 33677863631452242ULL) {
return std::numeric_limits<Timestamp>::min();
}
if (winTime >= 218145301729837567ULL) {
return std::numeric_limits<Timestamp>::max();;
}
// 1601 (Gregorian) 100-nsec
auto gregorian = static_cast<int64_t>(winTime);
// Convert filetime from 1601 epoch to 2000 epoch.
auto y2k = gregorian - TimeConst::WinFiletimeY2kDifference;
// Convert 100-nsec intervals into nsec intervals
return static_cast<Time::Timestamp>(y2k * 100LL);
}
uint64_t ToWinFiletime(Timestamp y2k) {
return (y2k + TimeConst::WinFiletimeY2kDifference) / 100ULL;
}
int32_t GetTimeElapsed(uint32_t start, uint32_t end) {
if (end < start) {
return ~start + end;
}
return end - start;
}
Timestamp GetTimestamp() {
return System_Time::Now();
}
uint64_t Nanoseconds() {
return System_Time::Nanoseconds();
}
uint64_t Microseconds() {
return System_Time::Microseconds();
}
uint64_t Milliseconds() {
return System_Time::Milliseconds();
}
uint64_t Seconds() {
return System_Time::Seconds();
}
Timestamp MakeTime(const TimeRec& date) {
Timestamp timestamp = 0;
#if defined(WHOA_SYSTEM_WIN)
// Win32 implementation
FILETIME fileTime = {};
SYSTEMTIME systemTime = {};
systemTime.wYear = static_cast<WORD>(date.year);
systemTime.wMonth = static_cast<WORD>(date.month);
systemTime.wDay = static_cast<WORD>(date.day);
systemTime.wHour = static_cast<WORD>(date.hour);
systemTime.wMinute = static_cast<WORD>(date.min);
systemTime.wSecond = static_cast<WORD>(date.sec);
systemTime.wMilliseconds = 0;
::SystemTimeToFileTime(&systemTime, &fileTime);
ULARGE_INTEGER ul = {};
ul.HighPart = fileTime.dwHighDateTime;
ul.LowPart = fileTime.dwLowDateTime;
timestamp = FromWinFiletime(ul.QuadPart);
#endif
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
// UNIX implementation
struct tm t;
t.tm_year = date.year - 1900;
t.tm_mon = date.month - 1;
t.tm_mday = date.day;
t.tm_hour = date.hour;
t.tm_min = date.min;
t.tm_sec = date.sec;
// Convert date into UNIX timestamp
auto unixTime = ::timegm(&t);
timestamp = FromUnixTime(unixTime);
#endif
// Add nsec to result
auto nsec = date.nsec;
// overflow check
if ((timestamp + static_cast<Timestamp>(nsec)) < timestamp) {
return timestamp;
}
timestamp += nsec;
return timestamp;
}
void BreakTime(Timestamp timestamp, TimeRec& date) {
auto mod = (timestamp % TimeConst::TimestampsPerSecond);
auto nsec = static_cast<uint32_t>(mod + TimeConst::TimestampsPerSecond);
if (mod < std::numeric_limits<uint32_t>::max()) {
nsec = mod;
}
#if defined(WHOA_SYSTEM_WIN)
// Win32 implementation
ULARGE_INTEGER ul = {};
ul.QuadPart = ToWinFiletime(timestamp);
FILETIME fileTime = {};
fileTime.dwHighDateTime = ul.HighPart;
fileTime.dwLowDateTime = ul.LowPart;
SYSTEMTIME systemTime = {};
::FileTimeToSystemTime(&fileTime, &systemTime);
date.day = static_cast<uint32_t>(systemTime.wDay);
date.hour = static_cast<uint32_t>(systemTime.wHour);
date.min = static_cast<uint32_t>(systemTime.wMinute);
date.sec = static_cast<uint32_t>(systemTime.wSecond);
date.wday = static_cast<uint32_t>(systemTime.wDayOfWeek);
date.year = static_cast<uint32_t>(systemTime.wYear);
date.nsec = static_cast<uint32_t>(nsec);
bool leapYear = (date.year % 400 == 0) || (date.year % 100 != 0 && ((systemTime.wYear & 3) == 0));
auto yearDay = s_monthDays[date.month] + -1 + static_cast<uint32_t>(systemTime.wDay);
date.yday = yearDay;
if (leapYear && date.month > 2) {
date.yday++;
}
#endif
#if defined(WHOA_SYSTEM_MAC) || defined(WHOA_SYSTEM_LINUX)
// UNIX implementation
auto unixTime = static_cast<time_t>(ToUnixTime(timestamp));
struct tm t;
::gmtime_r(&unixTime, &t);
date.year = t.tm_year + 1900;
date.month = t.tm_mon + 1;
date.day = t.tm_mday;
date.hour = t.tm_hour;
date.min = t.tm_min;
date.sec = t.tm_sec;
date.nsec = nsec;
date.wday = t.tm_wday;
date.yday = t.tm_yday;
#endif
}
} // namespace Time
} // namespace Blizzard

39
bc/time/Time.hpp Normal file
View file

@ -0,0 +1,39 @@
#ifndef BC_TIME_TIME_HPP
#define BC_TIME_TIME_HPP
#include "bc/time/Types.hpp"
#include <cstdint>
namespace Blizzard {
namespace Time {
int32_t ToUnixTime(Timestamp timestamp);
Timestamp FromUnixTime(int32_t unixTime);
// Win32 FILETIME to y2k
Timestamp FromWinFiletime(uint64_t winTime);
uint64_t ToWinFiletime(Timestamp y2k);
Timestamp GetTimestamp();
int32_t GetTimeElapsed(uint32_t start, uint32_t end);
Timestamp MakeTime(const TimeRec& date);
void BreakTime(Timestamp timestamp, TimeRec& date);
uint64_t Nanoseconds();
uint64_t Microseconds();
uint64_t Milliseconds();
uint64_t Seconds();
} // namespace Time
} // namespace Blizzard
#endif

19
bc/time/TimeConst.hpp Normal file
View file

@ -0,0 +1,19 @@
#ifndef BC_TIME_TIME_CONST_HPP
#define BC_TIME_TIME_CONST_HPP
#include <cstdint>
namespace TimeConst {
// The number of nanoseconds in a second
constexpr int64_t TimestampsPerSecond = 1000000000ULL;
// amount of win32 filetime units in a second
constexpr int64_t WinUnitsPerSecond = (TimestampsPerSecond / 100ULL);
// the FILETIME value needed to move from 1601 epoch to the Year 2000 epoch that Blizzard prefers
constexpr int64_t WinFiletimeY2kDifference = 125911584000000000ULL;
} // namespace TimeConst
#endif

28
bc/time/Types.hpp Normal file
View file

@ -0,0 +1,28 @@
#ifndef BC_TIME_TYPES_HPP
#define BC_TIME_TYPES_HPP
#include <cstdint>
namespace Blizzard {
namespace Time {
// Timestamp - nanoseconds starting from 0 == January 1 2000 00:00:00 GMT.
typedef int64_t Timestamp;
class TimeRec {
public:
int32_t year;
uint32_t month;
uint32_t day;
uint32_t hour;
uint32_t min;
uint32_t sec;
uint32_t nsec;
uint32_t wday;
uint32_t yday;
};
} // namespace Time
} // namespace Blizzard
#endif

41
test/Time.cpp Normal file
View file

@ -0,0 +1,41 @@
#include "bc/Time.hpp"
#include "bc/time/TimeConst.hpp"
#include "bc/Process.hpp"
#include "test/Test.hpp"
TEST_CASE("Blizzard::Time::FromUnixTime", "[time]") {
SECTION("convert zero Blizzard time from UNIX timestamp of Sat Jan 01 2000 00:00:00 GMT") {
auto stamp = Blizzard::Time::FromUnixTime(946684800);
REQUIRE(stamp == 0);
}
}
TEST_CASE("Blizzard::Time::ToUnixTime", "[time]") {
SECTION("convert zero Blizzard timestamp to Unix timestamp of Sat Jan 01 2000 00:00:00 GMT+0000") {
auto stamp = Blizzard::Time::ToUnixTime(0);
REQUIRE(stamp == 946684800);
}
}
TEST_CASE("Blizzard::Time::GetTimestamp", "[time]") {
SECTION("Get timestamp") {
auto now = Blizzard::Time::GetTimestamp();
REQUIRE(now > 0);
}
SECTION("Timestamp increases after 200 ms") {
auto t1 = Blizzard::Time::GetTimestamp();
Blizzard::Process::Sleep(200);
auto t2 = Blizzard::Time::GetTimestamp();
auto delta = t2 - t1;
REQUIRE(t2 > t1);
REQUIRE(delta >= 100000000);
}
}
TEST_CASE("Blizzard::Time::Nanoseconds", "[time]") {
SECTION("Read wall clock") {
auto wctime = Blizzard::Time::Nanoseconds();
REQUIRE(wctime > 0);
}
}