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.

---
 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 +++++++++++++++++++++++++
 6 files changed, 456 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

(limited to '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 <stdint.h>
+
+/// A particular point in time.
+#ifdef _WIN32
+typedef uint64_t time_point;
+#else
+#include <time.h>
+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 <stdlib.h>
+
+#ifdef _WIN32
+static const int64_t microseconds = 1000000;
+#endif
+static const int64_t nanoseconds = 1000000000;
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+#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 <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) ||     \
+    defined(__NetBSD__) || defined(__OpenBSD__)
+#define USE_SYSCTL_FOR_ARGS 1
+// clang-format off
+#include <sys/types.h>
+#include <sys/sysctl.h>
+// clang-format on
+#include <unistd.h>        // 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 <stdbool.h>
+
+#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 <stdio.h>
+
+#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