Files
Zag/src/kern/timecounter.zig
T
Loïc Grégoire 9e065ee023 initial commit
2026-03-21 22:47:06 -04:00

144 lines
4.6 KiB
Zig

const std = @import("std");
const rtl = @import("rtl");
const b = @import("base");
const ke = b.ke;
const ki = ke.private;
/// TimeCounter structure.
/// Represents a time-keeping source upon which the kernel can rely.
pub const TimeCounter = struct {
/// Callback that reads the current count.
read_count: *const fn () u64,
/// Frequency of the timer in Hz.
frequency: u64,
/// Name of the timer.
name: []const u8,
/// Quality of the timer.
/// Used to choose between two TimeCounters.
quality: i16,
/// Mask to apply to the counter value.
mask: u64,
/// Binary scaling exponent for fast ns conversion (0 if unused).
/// See `ticks_to_ns`.
p: u64,
/// Scaled nanoseconds-per-tick multiplier for fast ns conversion (0 if unused).
/// See `ticks_to_ns`.
n: u64,
};
var best_tc: ?*TimeCounter = null;
const TimeState = struct {
// Counter value at the time the current TimeCounter was selected.
// Used as the baseline for elapsed time calculation.
initial_count: u64,
// Accumulated nanosecond offset from previous TimeCounters and overflow events.
// Added to the current counter's contribution in `read_time_nano`.
offset: u64,
};
// SeqLock-protected time state.
// Avoids a race on `initial_count` and `offset` with `update_overflow`.
var state = rtl.SeqLock(TimeState).init(.{
.initial_count = 0,
.offset = 0,
});
// Convert a TimeCounter's count to nanoseconds.
// For 64-bit counters (`mask == maxInt(u64)`), uses a fast multiply-shift path
// via precomputed `p` and `n` to avoid 128-bit division at runtime.
// Falls back to a division-based conversion for narrower counters.
fn ticks_to_ns(tc: *TimeCounter, count: u64) u64 {
if (tc.p != 0) {
const val: u128 = @as(u128, @intCast(count)) * tc.n;
return @intCast(val >> @intCast(tc.p));
}
return (count * std.time.ns_per_s) / tc.frequency;
}
/// Register a time-keeping source.
/// The TimeCounter will be used if it is the highest quality one available.
/// This function cannot be safely called when the hardware timer has been started, as data races on `best_tc` are not avoided.
pub fn register(tc: *TimeCounter) void {
// Precompute scaling factors for the fast multiply-shift path.
// This is meant to avoid 128-bit division and only use multiplication for 64-bit counters.
if (tc.mask == std.math.maxInt(u64)) {
tc.p = std.math.log2_int_ceil(u64, tc.frequency);
tc.n = (@as(u64, std.time.ns_per_s) << @intCast(tc.p)) / tc.frequency;
} else {
tc.p = 0;
tc.n = 0;
}
// If the best TimeCounter changed, pass the baton:
// snapshot the old counter's elapsed time into `offset` and
// reset `initial_count` to the new counter's current value.
if (best_tc == null or tc.quality > best_tc.?.quality) {
const old_best = best_tc;
best_tc = tc;
if (old_best) |old| {
const old_count = old.read_count() & old.mask;
state.raw.offset = ticks_to_ns(old, old_count);
}
state.raw.initial_count = tc.read_count() & tc.mask;
}
}
/// Return the time elapsed since boot in nanoseconds.
pub fn read_time_nano() b.Nanoseconds {
const tc = best_tc orelse return 0;
const s = state.load();
const curr_count = tc.read_count() & tc.mask;
const elapsed = (curr_count - s.initial_count) & tc.mask;
return s.offset + ticks_to_ns(tc, elapsed);
}
/// Return the best TimeCounter.
pub fn best() ?*TimeCounter {
return best_tc;
}
/// Do a busy sleep of `ns` nanoseconds using TimeCounter.
pub fn sleep(ns: b.Nanoseconds) void {
const start = read_time_nano();
while (true) {
var cur = read_time_nano();
if (cur < start) continue;
if (cur - start >= ns) {
cur = read_time_nano();
// Check again as we may have overflowed and read a bogus value..
// This is kinda hacky but oh well.
if (cur - start >= ns) {
break;
}
}
std.atomic.spinLoopHint();
}
}
/// Called periodically to handle counter overflows.
pub fn update_overflow() void {
const tc = best_tc orelse return;
const s = state.load();
const cur_count = tc.read_count() & tc.mask;
if (s.initial_count > cur_count) {
// Counter has wrapped, use wrapping arithmetic to figure out by how much,
// then fold it into the offset and reset `initial_count`.
const overflowed_by = (cur_count -% s.initial_count) & tc.mask;
state.store(.{
.initial_count = cur_count,
.offset = s.offset + ticks_to_ns(tc, overflowed_by),
});
}
}