
// Program to read out the hardware counter and periodically print
// the offset between it and CLOCK_BOOTTIME/CLOCK_MONOTONIC.
// Morten Hauke Solvang, 2024

// For testing on my native architecture (x86_64):
//   clang hardware_counter.c -o hardware_counter -O2

// For testing on aarch64:
//   zig cc hardware_counter.c -o hardware_counter -O2 -target aarch64-linux-musl


// Example output:

// x86_64 before/after suspend
// Notice that the hardware counter has a much smaller value after resume.
// It appears that it gets reset to zero when the system is suspended.
//
// hardware counter value = 390006803165 cycles
// hardware counter freq. = 3500017733 Hz
// boot - monotonic       = 61.381029 sec
// boot - hardware        = 19065.295192 sec
// monotonic - hardware   = 19003.914163 sec
//
// hardware counter value = 10892287595 cycles
// hardware counter freq. = 3500017733 Hz
// boot - monotonic       = 70.185498 sec
// boot - hardware        = 19186.295364 sec
// monotonic - hardware   = 19116.109866 sec

// aarch64 before/after suspend
// Notice that (boot - monotonic) and (boot - hardware) change on suspend, but (monotonic - hardware) remains constant.
// It appears that CNTVCT is paused while the system is suspended, same as CLOCK_MONOTONIC.
//
// hardware counter value = 54417585512 cycles
// hardware counter freq. = 8000130 Hz
// boot - monotonic       = 173124.232899 sec
// boot - hardware        = 190057.524496 sec
// monotonic - hardware   = 16933.291596 sec
//
// hardware counter value = 54425737453 cycles
// hardware counter freq. = 8000130 Hz
// boot - monotonic       = 173140.993109 sec
// boot - hardware        = 190074.270763 sec
// monotonic - hardware   = 16933.277654 sec


// One thing I have not taken into account:
// There are mechanisms (used for virtualization) where it's possible to set an offset
// for the value read from the hardware counter. Could linux be doing this behind my back?



#include <time.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include <unistd.h>

static uint64_t
read_clock_ns(clockid_t clock)
{
    struct timespec timespec;
    clock_gettime(clock, &timespec);
    return (uint64_t)timespec.tv_sec*1000000000 + (uint64_t)timespec.tv_nsec;
}

static uint64_t
read_hardware_counter()
{
    uint64_t result;
    #if defined(__x86_64__)
        result = __rdtsc();
    #elif defined(__aarch64__)
        asm volatile("mrs %0, cntvct_el0" : "=r"(result) : : "memory");
    #else
        result = 0;
    #endif
    return result;
}


int main()
{
    uint64_t monotonic_0_ns = read_clock_ns(CLOCK_MONOTONIC);
    uint64_t hardware_counter_0 = read_hardware_counter();
    sleep(1);
    uint64_t monotonic_1_ns = read_clock_ns(CLOCK_MONOTONIC);
    uint64_t hardware_counter_1 = read_hardware_counter();

    // If we were just writing code for aarch64, we could have checked cntfrq_el0.
    // But on x86_64 it is very much non-trivial to get the frequency of rdtsc (see https://gist.github.com/pmttavara/6f06fc5c7679c07375483b06bb77430c).
    double estimated_hardware_counter_frequency = (double)(hardware_counter_1 - hardware_counter_0) / ((double)(monotonic_1_ns - monotonic_0_ns)/1e9);


    while (1) {
        uint64_t hardware_counter = read_hardware_counter();
        double boottime_sec = (double)read_clock_ns(CLOCK_BOOTTIME) / 1e9;
        double monotonic_sec = (double)read_clock_ns(CLOCK_MONOTONIC) / 1e9;
        double hardware_sec = (double)hardware_counter / estimated_hardware_counter_frequency;

        printf("hardware counter value = %lu cycles\n", hardware_counter);
        printf("hardware counter freq. = %.0f Hz\n", estimated_hardware_counter_frequency);
        printf("boot - monotonic       = %f sec\n", boottime_sec - monotonic_sec);
        printf("boot - hardware        = %f sec\n", boottime_sec - hardware_sec);
        printf("monotonic - hardware   = %f sec\n", monotonic_sec - hardware_sec);
        printf("\n");

        sleep(1);
    }

    return 0;
    
}