Coverage Report

Created: 2026-08-24 19:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
src/util/tsc.cpp
Line
Count
Source
1
#include <silk/util/tsc.h>
2
3
#include <silk/util/assert.h>
4
#include <silk/util/logger.h>
5
#include <silk/util/platform.h>
6
7
#include <cerrno>
8
#include <cstring>
9
10
#if defined(__x86_64__)
11
#    include <cpuid.h>
12
#endif
13
14
namespace silk
15
{
16
17
#if defined(__x86_64__)
18
19
// CPUID leaf 0x80000007 EDX bit 8 advertises the Invariant TSC feature:
20
// TSC runs at a constant rate across frequency transitions and remains synchronized
21
// across cores. Silk reads TSC on one CPU and compares the result on another
22
// (sleep deadlines, work-stealing budgets) -- if TSC is not invariant those
23
// comparisons are unsound. Older CPUs without this bit are not supported.
24
static bool hasInvariantTsc() noexcept
25
327
{
26
327
    uint32_t eax, ebx, ecx, edx;
27
327
    if (__get_cpuid(0x80000000, &eax, &ebx, &ecx, &edx) == 0 || eax < 0x80000007)
28
0
    {
29
0
        return false;
30
0
    }
31
327
    __cpuid(0x80000007, eax, ebx, ecx, edx);
32
327
    return (edx & (1u << 8)) != 0;
33
327
}
34
35
static uint64_t getTscFrequencyCpuid() noexcept
36
327
{
37
327
    uint32_t eax, ebx, ecx, edx;
38
327
    __cpuid(1, eax, ebx, ecx, edx);
39
40
    // Check for hypervisor presence (leaf 1 ECX bit 31).
41
327
    if (ecx & (1u << 31))
42
327
    {
43
327
        uint32_t hv_max;
44
327
        __cpuid(0x40000000, hv_max, ebx, ecx, edx);
45
46
327
        char vendor[13];
47
327
        memcpy(vendor + 0, &ebx, 4);
48
327
        memcpy(vendor + 4, &ecx, 4);
49
327
        memcpy(vendor + 8, &edx, 4);
50
327
        vendor[12] = '\0';
51
52
327
        if (strcmp(vendor, "KVMKVMKVM") == 0)
53
327
        {
54
            // KVM (AWS Nitro, GCP): leaf 0x40000010 EAX = TSC frequency in kHz.
55
327
            if (hv_max >= 0x40000010)
56
327
            {
57
327
                __cpuid(0x40000010, eax, ebx, ecx, edx);
58
327
                if (eax != 0)
59
327
                {
60
327
                    return static_cast<uint64_t>(eax) * 1000; // kHz -> Hz
61
327
                }
62
327
            }
63
327
        }
64
        // Hyper-V (Azure) does not expose a TSC frequency CPUID leaf;
65
        // fall through to the standard leaf 0x15 path below.
66
327
    }
67
68
    // Bare metal and Hyper-V (Azure): leaf 0x15 TSC/crystal clock ratio.
69
    //   EAX = denominator, EBX = numerator, ECX = crystal Hz (0 if not enumerated).
70
0
    uint32_t max_leaf;
71
0
    __get_cpuid(0, &max_leaf, &ebx, &ecx, &edx);
72
0
    if (max_leaf < 0x15)
73
0
    {
74
0
        return 0;
75
0
    }
76
77
0
    uint32_t tsc_denom, tsc_numer, crystal_hz;
78
0
    __cpuid_count(0x15, 0, tsc_denom, tsc_numer, crystal_hz, edx);
79
0
    if (tsc_denom == 0 || tsc_numer == 0)
80
0
    {
81
0
        return 0;
82
0
    }
83
84
0
    if (crystal_hz != 0)
85
0
    {
86
0
        return static_cast<uint64_t>(crystal_hz) * tsc_numer / tsc_denom; // Hz
87
0
    }
88
89
    // Crystal Hz not enumerated: leaf 0x16 EAX[15:0] gives the processor base
90
    // frequency in MHz, which equals the TSC frequency for an invariant TSC.
91
0
    if (max_leaf >= 0x16)
92
0
    {
93
0
        uint32_t base_mhz;
94
0
        __cpuid_count(0x16, 0, base_mhz, ebx, ecx, edx);
95
0
        base_mhz &= 0xffff;
96
0
        if (base_mhz != 0)
97
0
        {
98
0
            return static_cast<uint64_t>(base_mhz) * 1'000'000; // MHz -> Hz
99
0
        }
100
0
    }
101
102
0
    return 0;
103
0
}
104
105
static uint64_t getTscFrequencyCalibrated() noexcept
106
0
{
107
0
    uint64_t startNs = getTimeNanoseconds();
108
0
    uint64_t startCycles = Tsc::getCycles();
109
0
    uint64_t deadlineNs = startNs + 50'000'000; // 50 ms
110
111
0
    struct timespec deadline;
112
0
    deadline.tv_sec = deadlineNs / 1'000'000'000;
113
0
    deadline.tv_nsec = deadlineNs % 1'000'000'000;
114
115
0
    while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, nullptr) == EINTR)
116
0
    {
117
0
    }
118
119
0
    uint64_t durationNs = getTimeNanoseconds() - startNs;
120
0
    uint64_t durationCycles = Tsc::getCycles() - startCycles;
121
0
    return durationCycles * 1'000'000'000 / durationNs;
122
0
}
123
124
#endif // __x86_64__
125
126
void Tsc::initialize() noexcept
127
327
{
128
327
    if (frequency)
129
0
    {
130
        // Skip the second initialization.
131
0
        return;
132
0
    }
133
134
327
#if defined(__x86_64__)
135
    // Invariant TSC guarantees the counter ticks at a constant rate and stays
136
    // synchronized across cores -- silk reads TSC on one core and compares on
137
    // another, so this is what we want. Some virtualized environments (notably
138
    // KVM guests without nonstop_tsc) fail to advertise the bit via CPUID even
139
    // though rdtsc is usable, so warn and continue rather than aborting;
140
    // frequency then comes from CLOCK_MONOTONIC calibration below.
141
327
    if (!hasInvariantTsc())
142
0
    {
143
0
        SILK_WARN(
144
0
            "CPU does not advertise Invariant TSC; proceeding anyway "
145
0
            "-- cross-core timing may be unreliable (common on KVM guests)");
146
0
    }
147
327
    frequency = getTscFrequencyCpuid();
148
    // AMD CPUs do not populate CPUID leaves 0x15 or 0x16, and some older Intel
149
    // parts leave them empty too. Fall back to measuring TSC against
150
    // CLOCK_MONOTONIC over a fixed window -- the same approach the Linux kernel
151
    // takes when CPUID does not advertise a frequency. This is also the path
152
    // taken on hosts without an invariant TSC, where the CPUID frequency leaves
153
    // are typically absent as well.
154
327
    if (frequency == 0)
155
0
    {
156
0
        frequency = getTscFrequencyCalibrated();
157
0
    }
158
#elif defined(__aarch64__)
159
    // ARMv8 cntvct_el0 is anchored to a system counter that is invariant by
160
    // architecture, so no equivalent capability check is required.
161
    __asm__ volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
162
#else
163
#    error Unsupported platform
164
#endif
165
166
327
    SILK_ASSERT(frequency != 0, "failed to determine TSC frequency");
167
327
    nsPerCycleFp = (1ULL << SHIFT) * 1'000'000'000 / frequency;
168
327
    cyclesPerNsFp = frequency * (1ULL << SHIFT) / 1'000'000'000;
169
327
    maxCyclesBeforeMultiplyOverflow = UINT64_MAX / nsPerCycleFp;
170
    maxNanosecondsBeforeMultiplyOverflow = UINT64_MAX / cyclesPerNsFp;
171
327
}
172
173
} // namespace silk