HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
time_stamp_utc.hpp
1// Copyright Take Vos 2021.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "chrono.hpp"
8#include "time_stamp_count.hpp"
9#include "../utility/utility.hpp"
10#include "../concurrency/concurrency.hpp"
11#include "../macros.hpp"
12#include <compare>
13#include <utility>
14#include <array>
15#include <atomic>
16#include <thread>
17#include <ranges>
18#include <algorithm>
19#include <numeric>
20#include <mutex>
21#include <chrono>
22
23hi_export_module(hikogui.time.time_stamp_utc);
24
25
26hi_export namespace hi::inline v1 {
27
34 [[nodiscard]] static utc_nanoseconds now(time_stamp_count& tsc)
35 {
36 auto shortest_diff = std::numeric_limits<uint64_t>::max();
37 time_stamp_count shortest_tsc;
38 utc_nanoseconds shortest_tp;
39
40 // With three samples gathered on the same CPU we should
41 // have a TSC/UTC/TSC combination that was run inside a single time-slice.
42 for (auto i = 0; i != 10; ++i) {
43 auto const tmp_tsc1 = time_stamp_count::now();
44 auto const tmp_tp = std::chrono::utc_clock::now();
45 auto const tmp_tsc2 = time_stamp_count::now();
46
47 if (tmp_tsc1.cpu_id() != tmp_tsc2.cpu_id()) {
48 throw os_error("CPU Switch detected during get_sample(), which should never happen");
49 }
50
51 if (tmp_tsc1.count() > tmp_tsc2.count()) {
52 // TSC skipped backwards, this may happen when the TSC of multiple
53 // CPUs get synchronized with each other.
54 // For example when waking up from sleep.
55 continue;
56 }
57
58 auto const diff = tmp_tsc2.count() - tmp_tsc1.count();
59
60 if (diff < shortest_diff) {
61 shortest_diff = diff;
62 shortest_tp = tmp_tp;
63 shortest_tsc = tmp_tsc1 + (diff / 2);
64 }
65 }
66
67 if (shortest_diff == std::numeric_limits<uint64_t>::max()) {
68 throw os_error("Unable to get TSC sample.");
69 }
70
71 tsc = shortest_tsc;
72 return shortest_tp;
73 }
74
83 [[nodiscard]] static utc_nanoseconds make(time_stamp_count const& tsc) noexcept
84 {
85 auto i = tsc.cpu_id();
86 if (i >= 0) {
87 auto const tsc_epoch = tsc_epochs[i].load(std::memory_order::relaxed);
88 if (tsc_epoch != utc_nanoseconds{}) {
89 return tsc_epoch + tsc.time_since_epoch();
90 }
91 }
92
93 // Fallback.
94 auto const ref_tp = std::chrono::utc_clock::now();
95 auto const ref_tsc = time_stamp_count::now();
96 auto const diff_ns = ref_tsc.time_since_epoch() - tsc.time_since_epoch();
97 return ref_tp - diff_ns;
98 }
99
102 static bool start_subsystem() noexcept
103 {
104 return hi::start_subsystem(global_state_type::time_stamp_utc_is_running, init_subsystem, deinit_subsystem);
105 }
106
109 static void stop_subsystem() noexcept
110 {
111 return hi::stop_subsystem(deinit_subsystem);
112 }
113
120 static void adjust_for_drift() noexcept;
121
122private:
123 inline static std::jthread subsystem_thread;
124 inline static unfair_mutex mutex;
125 inline static std::array<std::atomic<utc_nanoseconds>, maximum_num_cpus> tsc_epochs = {};
126
127 static void subsystem_proc_frequency_calibration(std::stop_token stop_token)
128 {
129 using namespace std::chrono_literals;
130
131 // Calibrate the TSC frequency to within 1 ppm.
132 // A 1s measurement already brings is to about 1ppm. We are
133 // going to be taking average of the IQR of 11 samples, just
134 // in case there are UTC clock adjustment made during the measurement.
135
136 std::array<uint64_t, 16> frequencies;
137 for (auto i = 0; i != frequencies.size();) {
138 auto const f = time_stamp_count::measure_frequency(1s);
139 if (f != 0) {
140 frequencies[i] = f;
141 ++i;
142 }
143
144 if (stop_token.stop_requested()) {
145 return;
146 }
147 }
148 std::ranges::sort(frequencies);
149 auto const iqr_size = frequencies.size() / 2;
150 auto const iqr_first = std::next(frequencies.cbegin(), frequencies.size() / 4);
151 auto const iqr_last = std::next(iqr_first, iqr_size);
152 auto const frequency = std::accumulate(iqr_first, iqr_last, uint64_t{0}) / iqr_size;
153
154 time_stamp_count::set_frequency(frequency);
155 }
156 static void subsystem_proc(std::stop_token stop_token)
157 {
158 using namespace std::chrono_literals;
159
160 set_thread_name("time_stamp_utc");
161 subsystem_proc_frequency_calibration(stop_token);
162
163 auto const process_cpu_mask = process_affinity_mask();
164
165 std::size_t next_cpu = 0;
166 while (not stop_token.stop_requested()) {
167 auto const current_cpu = advance_thread_affinity(next_cpu);
168
170 auto const lock = std::scoped_lock(time_stamp_utc::mutex);
171
172 time_stamp_count tsc;
173 auto const tp = time_stamp_utc::now(tsc);
174 hi_assert(tsc.cpu_id() == narrow_cast<ssize_t>(current_cpu));
175
176 tsc_epochs[current_cpu].store(tp - tsc.time_since_epoch(), std::memory_order::relaxed);
177 }
178 }
179
182 static bool init_subsystem() noexcept
183 {
184 time_stamp_utc::subsystem_thread = std::jthread{subsystem_proc};
185 return true;
186 }
187
190 static void deinit_subsystem() noexcept
191 {
192 if (global_state_disable(global_state_type::time_stamp_utc_is_running)) {
193 if (time_stamp_utc::subsystem_thread.joinable()) {
194 time_stamp_utc::subsystem_thread.request_stop();
195 time_stamp_utc::subsystem_thread.join();
196 }
197 }
198 }
199
200 [[nodiscard]] static std::size_t find_cpu_id(uint32_t cpu_id) noexcept;
201};
202
203constexpr std::string format_engineering(std::chrono::nanoseconds duration)
204{
205 using namespace std::chrono_literals;
206
207 if (duration >= 1s) {
208 return std::format("{:.3g}s ", narrow_cast<double>(duration / 1ns) / 1'000'000'000);
209 } else if (duration >= 1ms) {
210 return std::format("{:.3g}ms", narrow_cast<double>(duration / 1ns) / 1'000'000);
211 } else if (duration >= 1us) {
212 return std::format("{:.3g}us", narrow_cast<double>(duration / 1ns) / 1'000);
213 } else {
214 return std::format("{:.3g}ns", narrow_cast<double>(duration / 1ns));
215 }
216}
217
218} // namespace hi::inline v1
std::vector< bool > process_affinity_mask()
Get the current process CPU affinity mask.
void set_thread_name(std::string_view name) noexcept
Set the name of the current thread.
T::value_type start_subsystem(T &check_variable, typename T::value_type off_value, typename T::value_type(*init_function)(), void(*deinit_function)())
Start a sub-system.
Definition subsystem.hpp:117
bool global_state_disable(global_state_type subsystem, std::memory_order order=std::memory_order::seq_cst) noexcept
Disable a subsystem.
Definition global_state.hpp:249
std::size_t advance_thread_affinity(std::size_t &cpu) noexcept
Advance thread affinity to the next CPU.
Definition thread_intf.hpp:121
void stop_subsystem(void(*deinit_function)())
Stop a sub-system.
Definition subsystem.hpp:203
STL namespace.
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
Since Window's 10 QueryPerformanceCounter() counts at only 10MHz which is too low to measure performa...
Definition time_stamp_count.hpp:36
Timestamp.
Definition time_stamp_utc.hpp:30
static void stop_subsystem() noexcept
This will stop the calibration subsystem.
Definition time_stamp_utc.hpp:109
static bool start_subsystem() noexcept
This will start the calibration subsystem.
Definition time_stamp_utc.hpp:102
static utc_nanoseconds make(time_stamp_count const &tsc) noexcept
Make a time point from a time stamp count.
Definition time_stamp_utc.hpp:83
static utc_nanoseconds now(time_stamp_count &tsc)
Get the current time and TSC value.
Definition time_stamp_utc.hpp:34
static void adjust_for_drift() noexcept
A calibration step which will drift the per-cpu tsc-offset.
T accumulate(T... args)
T cbegin(T... args)
T lock(T... args)
T max(T... args)
T next(T... args)
T size(T... args)
T sleep_for(T... args)