From cc96d69ed11c60a782cd8b993d4bdf2ce8c99560 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 4 Dec 2021 18:31:18 -0800 Subject: Add timer library. --- CMakeLists.txt | 1 + timer/CMakeLists.txt | 23 ++++++ timer/README.md | 2 + timer/include/timer.h | 55 ++++++++++++++ timer/src/timer.c | 96 +++++++++++++++++++++++++ timer/test/test.h | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ timer/test/timer_test.c | 95 +++++++++++++++++++++++++ 7 files changed, 457 insertions(+) create mode 100644 timer/CMakeLists.txt create mode 100644 timer/README.md create mode 100644 timer/include/timer.h create mode 100644 timer/src/timer.c create mode 100644 timer/test/test.h create mode 100644 timer/test/timer_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 36df847..9b97945 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,3 +7,4 @@ add_subdirectory(list) add_subdirectory(listpool) add_subdirectory(log) add_subdirectory(mempool) +add_subdirectory(timer) diff --git a/timer/CMakeLists.txt b/timer/CMakeLists.txt new file mode 100644 index 0000000..d915163 --- /dev/null +++ b/timer/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.10) + +project(timer) + +# Library + +set(SRC + src/timer.c) + +add_library(timer ${SRC}) + +target_include_directories(timer PUBLIC include) + +target_compile_options(timer PRIVATE -Wall -Wextra) + +# Test + +add_executable(timer_test + test/timer_test.c) + +target_link_libraries(timer_test timer) + +target_compile_options(timer_test PRIVATE -DUNIT_TEST -DNDEBUG -Wall -Wextra) diff --git a/timer/README.md b/timer/README.md new file mode 100644 index 0000000..14855e5 --- /dev/null +++ b/timer/README.md @@ -0,0 +1,2 @@ +# timer +Cross-platform, high precision timer. diff --git a/timer/include/timer.h b/timer/include/timer.h new file mode 100644 index 0000000..0274c69 --- /dev/null +++ b/timer/include/timer.h @@ -0,0 +1,55 @@ +#pragma once + +#include + +/// A particular point in time. +#ifdef _WIN32 +typedef uint64_t time_point; +#else +#include +typedef struct timespec time_point; +#endif + +/// Time elapsed between two time points. +typedef uint64_t time_delta; + +/// A high resolution timer. +typedef struct { + time_point start_time; // The instant the timer was last started. + time_point last_tick; // The instant the timer was last ticked. + time_delta running_time; // Time elapsed since the timer was last started. + time_delta delta_time; // Time elapsed since the last tick. +} Timer; + +/// Construct a new timer. +Timer timer_make(void); + +/// Start the timer. +/// This sets the time point from which time deltas are measured. +/// Calling this multilple times resets the timer. +void timer_start(Timer*); + +/// Update the timer's running and delta times. +void timer_tick(Timer*); + +/// Get the current time. +time_point time_now(void); + +/// Return the time elapsed between two timestamps. +time_delta time_diff(time_point start, time_point end); + +/// Return the time elapsed in seconds. +double time_delta_to_sec(time_delta dt); + +/// Convert the time elapsed in seconds to a time delta. +time_delta sec_to_time_delta(double seconds); + +/// Put the caller thread to sleep for the given amount of time. +void time_sleep(time_delta dt); + +/// The time point 0. +#ifdef _WIN32 +static const time_point time_zero = 0; +#else +static const time_point time_zero = {0, 0}; +#endif diff --git a/timer/src/timer.c b/timer/src/timer.c new file mode 100644 index 0000000..2ee8ef5 --- /dev/null +++ b/timer/src/timer.c @@ -0,0 +1,96 @@ +#include "timer.h" + +#include + +#ifdef _WIN32 +static const int64_t microseconds = 1000000; +#endif +static const int64_t nanoseconds = 1000000000; + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#ifdef _WIN32 +static double seconds_per_count; +#endif + +static void timer_initialise() { +#ifdef _WIN32 + __int64 counts_per_sec; + QueryPerformanceFrequency((LARGE_INTEGER*)&counts_per_sec); + seconds_per_count = 1.0 / (double)counts_per_sec; +#endif +} + +Timer timer_make(void) { + timer_initialise(); + Timer timer = {0}; + timer_start(&timer); + return timer; +} + +void timer_start(Timer* timer) { + timer->start_time = time_now(); + timer->last_tick = timer->start_time; + timer->running_time = 0; + timer->delta_time = 0; +} + +void timer_tick(Timer* timer) { + const time_point this_tick = time_now(); + timer->running_time = time_diff(timer->start_time, this_tick); + timer->delta_time = time_diff(timer->last_tick, this_tick); + timer->last_tick = this_tick; +} + +time_point time_now(void) { + time_point t; +#ifdef _WIN32 + QueryPerformanceCounter((LARGE_INTEGER*)&t); +#else + clock_gettime(CLOCK_REALTIME, &t); +#endif + return t; +} + +time_delta time_diff(time_point start, time_point end) { +#ifdef _WIN32 + // Force nonnegative. The DXSDK's CDXUTTimer mentions that if the + // processor goes into a power save mode or we get shuffled to + // another processor, then the delta time can be negative. + return std::max(0, end - start); +#else + return (end.tv_sec - start.tv_sec) * 1e9 + (end.tv_nsec - start.tv_nsec); +#endif +} + +double time_delta_to_sec(time_delta dt) { +#ifdef _WIN32 + return (double)dt * seconds_per_count; +#else + return (double)dt * 1.0e-9; +#endif +} + +time_delta sec_to_time_delta(double seconds) { +#ifdef _WIN32 + return (time_delta)(seconds / seconds_per_count); +#else + return (time_delta)(seconds * 1.0e9); +#endif +} + +void time_sleep(time_delta dt) { +#ifdef _WIN32 + const int64_t ms = dt / microseconds; + Sleep((DWORD)(ms)); +#else + const int64_t sec = dt / nanoseconds; + struct timespec ts; + ts.tv_sec = (long)sec; + ts.tv_nsec = (long)(dt % nanoseconds); + nanosleep(&ts, NULL); +#endif +} diff --git a/timer/test/test.h b/timer/test/test.h new file mode 100644 index 0000000..fd8dc22 --- /dev/null +++ b/timer/test/test.h @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#ifdef UNIT_TEST + +#include +#include +#include +#include + +#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \ + defined(__NetBSD__) || defined(__OpenBSD__) +#define USE_SYSCTL_FOR_ARGS 1 +// clang-format off +#include +#include +// clang-format on +#include // getpid +#endif + +struct test_file_metadata; + +struct test_failure { + bool present; + const char *message; + const char *file; + int line; +}; + +struct test_case_metadata { + void (*fn)(struct test_case_metadata *, struct test_file_metadata *); + struct test_failure failure; + const char *name; + struct test_case_metadata *next; +}; + +struct test_file_metadata { + bool registered; + const char *name; + struct test_file_metadata *next; + struct test_case_metadata *tests; +}; + +struct test_file_metadata __attribute__((weak)) * test_file_head; + +#define SET_FAILURE(_message) \ + metadata->failure = (struct test_failure) { \ + .message = _message, .file = __FILE__, .line = __LINE__, .present = true, \ + } + +#define TEST_EQUAL(a, b) \ + do { \ + if ((a) != (b)) { \ + SET_FAILURE(#a " != " #b); \ + return; \ + } \ + } while (0) + +#define TEST_TRUE(a) \ + do { \ + if (!(a)) { \ + SET_FAILURE(#a " is not true"); \ + return; \ + } \ + } while (0) + +#define TEST_STREQUAL(a, b) \ + do { \ + if (strcmp(a, b) != 0) { \ + SET_FAILURE(#a " != " #b); \ + return; \ + } \ + } while (0) + +#define TEST_CASE(_name) \ + static void __test_h_##_name(struct test_case_metadata *, \ + struct test_file_metadata *); \ + static struct test_file_metadata __test_h_file; \ + static struct test_case_metadata __test_h_meta_##_name = { \ + .name = #_name, \ + .fn = __test_h_##_name, \ + }; \ + static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \ + __test_h_meta_##_name.next = __test_h_file.tests; \ + __test_h_file.tests = &__test_h_meta_##_name; \ + if (!__test_h_file.registered) { \ + __test_h_file.name = __FILE__; \ + __test_h_file.next = test_file_head; \ + test_file_head = &__test_h_file; \ + __test_h_file.registered = true; \ + } \ + } \ + static void __test_h_##_name( \ + struct test_case_metadata *metadata __attribute__((unused)), \ + struct test_file_metadata *file_metadata __attribute__((unused))) + +extern void __attribute__((weak)) (*test_h_unittest_setup)(void); +/// Run defined tests, return true if all tests succeeds +/// @param[out] tests_run if not NULL, set to whether tests were run +static inline void __attribute__((constructor(102))) run_tests(void) { + bool should_run = false; +#ifdef USE_SYSCTL_FOR_ARGS + int mib[] = { + CTL_KERN, +#if defined(__NetBSD__) || defined(__OpenBSD__) + KERN_PROC_ARGS, + getpid(), + KERN_PROC_ARGV, +#else + KERN_PROC, + KERN_PROC_ARGS, + getpid(), +#endif + }; + char *arg = NULL; + size_t arglen; + sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &arglen, NULL, 0); + arg = malloc(arglen); + sysctl(mib, sizeof(mib) / sizeof(mib[0]), arg, &arglen, NULL, 0); +#else + FILE *cmdlinef = fopen("/proc/self/cmdline", "r"); + char *arg = NULL; + int arglen; + fscanf(cmdlinef, "%ms%n", &arg, &arglen); + fclose(cmdlinef); +#endif + for (char *pos = arg; pos < arg + arglen; pos += strlen(pos) + 1) { + if (strcmp(pos, "--unittest") == 0) { + should_run = true; + break; + } + } + free(arg); + + if (!should_run) { + return; + } + + if (&test_h_unittest_setup) { + test_h_unittest_setup(); + } + + struct test_file_metadata *i = test_file_head; + int failed = 0, success = 0; + while (i) { + fprintf(stderr, "Running tests from %s:\n", i->name); + struct test_case_metadata *j = i->tests; + while (j) { + fprintf(stderr, "\t%s ... ", j->name); + j->failure.present = false; + j->fn(j, i); + if (j->failure.present) { + fprintf(stderr, "failed (%s at %s:%d)\n", j->failure.message, + j->failure.file, j->failure.line); + failed++; + } else { + fprintf(stderr, "passed\n"); + success++; + } + j = j->next; + } + fprintf(stderr, "\n"); + i = i->next; + } + int total = failed + success; + fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total, + failed, total); + exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +} + +#else + +#include + +#define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) + +#define TEST_EQUAL(a, b) \ + (void)(a); \ + (void)(b) +#define TEST_TRUE(a) (void)(a) +#define TEST_STREQUAL(a, b) \ + (void)(a); \ + (void)(b) + +#endif diff --git a/timer/test/timer_test.c b/timer/test/timer_test.c new file mode 100644 index 0000000..a220ead --- /dev/null +++ b/timer/test/timer_test.c @@ -0,0 +1,95 @@ +#include "timer.h" + +#include + +#include "test.h" + +// Sleep puts the calling thread to sleep for a time >= than the given time. +TEST_CASE(sleep) { + const double sleep_time_sec = 0.1; + + const time_point start = time_now(); + time_sleep(sec_to_time_delta(sleep_time_sec)); + const time_point end = time_now(); + + TEST_TRUE(time_delta_to_sec(time_diff(start, end)) >= sleep_time_sec); +} + +// The timer starts as soon as it is created. +TEST_CASE(start_on_new) { + const time_point test_start_time = time_now(); + Timer timer = timer_make(); + TEST_TRUE(time_delta_to_sec(time_diff(test_start_time, timer.start_time)) >= + 0.0); +} + +// Tick updates the timer's last tick time. +TEST_CASE(tick_updates_last_tick_time) { + const double sleep_time_sec = 0.1; + + Timer timer = timer_make(); + time_sleep(sec_to_time_delta(sleep_time_sec)); + timer_tick(&timer); + + TEST_TRUE(time_delta_to_sec(time_diff(timer.start_time, timer.last_tick)) >= + sleep_time_sec); +} + +// Tick updates the timer's delta time. +TEST_CASE(tick_updates_delta_time) { + const double sleep_time_sec = 0.1; + + Timer timer = timer_make(); + time_sleep(sec_to_time_delta(sleep_time_sec)); + timer_tick(&timer); + + TEST_TRUE(time_delta_to_sec(timer.delta_time) >= sleep_time_sec); +} + +// Tick leaves the timer's start time unchanged. +TEST_CASE(tick_does_not_change_start_time) { + Timer timer = timer_make(); + const time_point start_time = timer.start_time; + time_sleep(sec_to_time_delta(0.1)); + timer_tick(&timer); + TEST_TRUE(time_delta_to_sec(time_diff(start_time, timer.start_time)) == 0.0); +} + +// Start starts/restarts the timer and updates the timer's start time. +TEST_CASE(start_restarts_start_time) { + const double sleep_time_seconds = 0.1; + Timer timer = timer_make(); + const time_point start_time = timer.start_time; + time_sleep(sec_to_time_delta(sleep_time_seconds)); + timer_start(&timer); + TEST_TRUE(time_delta_to_sec(time_diff(start_time, timer.start_time)) >= + sleep_time_seconds); +} + +// Count the number of hundred-seconds in a second. +TEST_CASE(count) { + Timer timer = timer_make(); + + int hundred_seconds = 0; + const time_point start = timer.start_time; + { + while (time_delta_to_sec(timer.running_time) <= 1.0) { + hundred_seconds++; + time_sleep(sec_to_time_delta(0.1)); + timer_tick(&timer); + TEST_TRUE(time_delta_to_sec(timer.delta_time) >= 0.1); + } + } + const time_point end = time_now(); + + const double time_elapsed = time_delta_to_sec(time_diff(start, end)); + + TEST_EQUAL(hundred_seconds, 10); + TEST_TRUE(time_elapsed >= 1.0); +} + +int main(int argc, const char** argv) { + (void)argc; // Unused. + printf("Usage: %s --unittest\n", argv[0]); + return 0; +} -- cgit v1.2.3