Skip to content

Measurement Precision

Every timing measurement has a precision limit: the smallest timing difference that can be reliably detected. This page explains how measurement precision affects your tests and what to do when you can’t achieve the precision you want.

Three factors determine the smallest timing difference you can detect:

  1. Timer resolution: How finely the hardware timer counts time. A timer that ticks every 42ns cannot distinguish differences smaller than that.

  2. System noise: Background processes, CPU frequency changes, and cache effects add random variation to measurements. This noise makes small timing differences hard to distinguish from random fluctuation.

  3. Sample count: More samples reduce the impact of noise through averaging, improving precision.

The library combines these factors into a measurement floor (θ_floor): the minimum detectable effect for your particular setup.

When you request a threshold (say, SharedHardware at 0.6ns), the library checks whether that precision is actually achievable. If not, it automatically elevates the threshold to something measurable.

Example scenario:

You’re testing on Apple Silicon without elevated privileges:

  • Timer resolution: 42ns (standard cntvct_el0 counter at 24 MHz)
  • After accounting for noise and sample count, θ_floor ≈ 21ns

You requested SharedHardware (0.6ns), but:

  • The library cannot distinguish 0.6ns differences with a 42ns timer
  • The effective threshold becomes 21ns instead
  • Results tell you whether there’s a leak above 21ns, not 0.6ns

This isn’t a bug; it’s honest reporting. The library could pretend to test at 0.6ns and give you a false sense of confidence, but instead it tells you what precision was actually achieved.

tacet supports multiple timing sources across platforms and languages. This table shows all available timers, including both standard and cycle-accurate options:

LanguagePlatformTimerPrecisionRequirementsNotes
Rustx86_64rdtsc~0.3–1nsNoneCPU timestamp counter (cycle-accurate)
RustmacOS ARM64cntvct_el0~42nsNone24 MHz virtual timer (default)
RustmacOS ARM64kperf PMU~0.3–1nssudo + single-threadedCycle-accurate via Apple’s kperf framework
Enable: TimerSpec::CyclePrecision
RustLinux ARM64cntvct_el018–42ns or ~1nsNoneSoC-dependent (ARMv8.6+ is ~1ns)
RustLinux ARM64perf_event~0.3–1nssudo or CAP_PERFMONCycle-accurate via Linux perf_event API
Enable: TimerSpec::CyclePrecision
RustOtherInstant::now()~100ns+NoneOS-dependent fallback
C/C++x86_64rdtsc~0.3–1nsNoneInline assembly (cycle-accurate)
C/C++macOS ARM64cntvct_el0~42nsNoneInline assembly (24 MHz virtual timer)
C/C++macOS ARM64kperf PMU~0.3–1nssudo + single-threadedCycle-accurate (planned)
C/C++Linux ARM64cntvct_el018–42ns or ~1nsNoneInline assembly (SoC-dependent)
C/C++Linux ARM64perf_event~0.3–1nssudo or CAP_PERFMONCycle-accurate (planned)
Gox86_64rdtsc~0.3–1nsNoneAssembly (cycle-accurate)
GomacOS ARM64cntvct_el0~42nsNoneAssembly (24 MHz virtual timer)
GomacOS ARM64kperf PMU~0.3–1nssudo + single-threadedCycle-accurate (planned)
GoLinux ARM64cntvct_el018–42ns or ~1nsNoneAssembly (SoC-dependent)
GoLinux ARM64perf_event~0.3–1nssudo or CAP_PERFMONCycle-accurate (planned)
GoOthertime.Now()~100ns+NoneOS-dependent fallback
JavaScript/WASMLinuxclock_gettime(CLOCK_MONOTONIC)~1–10nsNoneVia Bun.nanoseconds() or process.hrtime.bigint()
vDSO fast path
JavaScript/WASMmacOSmach_absolute_time()~10–100nsNoneVia Bun.nanoseconds() or process.hrtime.bigint()
JavaScript/WASMWindowsQueryPerformanceCounter()~10–100nsNoneVia Bun.nanoseconds() or process.hrtime.bigint()

Key points:

  • Automatic timer selection: By default, tacet automatically selects the best available timer for your platform. You don’t need to configure anything unless you specifically want cycle-accurate timers on ARM64.
  • Precision notification: If the available timer precision isn’t sufficient to measure at your requested threshold (e.g., SharedHardware at 0.6ns on Apple Silicon with standard timer), tacet will:
    1. Notify you that the threshold was elevated to match measurement capabilities
    2. Report the actual threshold tested in the results (theta_eff vs theta_user)
    3. Still give you valid results at the achievable precision level
  • x86_64: Cycle-accurate by default via rdtsc — no special setup needed across all languages
  • ARM64 standard timers: Use cntvct_el0 virtual timer (42ns on Apple Silicon, varies on other SoCs)
  • ARM64 cycle-accurate timers (bold rows): Require elevated privileges but provide ~0.3–1ns precision
    • macOS: kperf PMU requires both sudo AND --test-threads=1 (single-threaded restriction)
    • Linux: perf_event requires sudo OR CAP_PERFMON capability
    • Rust: Available now via TimerSpec::CyclePrecision
    • C/C++/Go: Planned support
  • JavaScript/WASM: Cannot access CPU cycle counters; relies on OS monotonic clocks (still sub-microsecond)

Most users don’t need cycle-accurate timers. Consider your attacker model:

Attacker ModelThresholdAchievable With
RemoteNetwork50 μs (~50,000ns)✓ All standard timers on all platforms
AdjacentNetwork100ns✓ All standard timers on all platforms
SharedHardware0.6ns (~2 cycles)✓ x86_64 standard timer
✓ ARM64 cycle-accurate timer (kperf/perf_event)
Research0ns (any difference)✓ x86_64 standard timer
✓ ARM64 cycle-accurate timer (kperf/perf_event)

Recommendations:

  • Most users: Standard timers are sufficient. AdjacentNetwork (100ns) works perfectly with all standard timers, including Apple Silicon’s 42ns timer.
  • Testing SharedHardware on ARM64: Use cycle-accurate timers (kperf/perf_event). Currently available in Rust; planned for C/C++/Go.
  • JavaScript/WASM users: Standard timers only. Use AdjacentNetwork threshold, or test on x86_64 for better precision.
  • CI/cloud environments: Expect higher measurement floors due to virtualization noise; AdjacentNetwork is a practical threshold.

If your tests are hitting the measurement floor, you have several options:

Section titled “Enable cycle-accurate timers (recommended)”

Use the CyclePrecision timer preset to request cycle-accurate measurements:

use tacet::{TimingOracle, AttackerModel, TimerSpec, helpers::InputPair};
let inputs = InputPair::new(
|| [0u8; 32],
|| rand::random::<[u8; 32]>()
);
TimingOracle::for_attacker(AttackerModel::SharedHardware)
.timer_spec(TimerSpec::CyclePrecision) // Explicitly request cycle-accurate timer
.test(inputs, |data| my_function(data));

Cycle-accurate timers require elevated privileges on ARM64:

Terminal window
# kperf requires both sudo AND single-threaded execution
sudo -E cargo test --test my_tests -- --test-threads=1

The --test-threads=1 is required because macOS kperf can only be accessed by one thread at a time.

If you can’t use cycle-accurate timers, consider whether the elevated threshold is acceptable:

  • θ_floor of 21ns still catches most timing leaks
  • AdjacentNetwork (100ns) works well even with coarse timers
  • Only SharedHardware (0.6ns) truly requires cycle-accurate precision

Set a threshold that’s above your measurement floor:

use tacet::{TimingOracle, AttackerModel};
// If your floor is ~21ns, test at 25ns
TimingOracle::for_attacker(AttackerModel::Custom { threshold_ns: 25.0 })
.test(inputs, |data| my_function(data));

This gives you conclusive results at an achievable precision level.

The SharedHardware preset (0.6ns, ~2 CPU cycles) is designed for detecting cycle-level leaks exploitable in SGX enclaves, containers, or cross-VM attacks. It’s achievable on:

  • x86_64: Works with the standard rdtsc timer
  • ARM64 with cycle-accurate timer: Works when running with sudo (+ --test-threads=1 on macOS)
  • ARM64 without cycle-accurate timer: Not achievable (θ_floor is ~21ns)

If you need SharedHardware-level precision but can’t achieve it, consider:

  1. Testing on x86_64 hardware where rdtsc provides inherent precision
  2. Using Custom { threshold_ns: 10.0 } as a realistic “shared hardware” threshold for ARM64 without cycle-accurate timing
  3. Accepting that some ultra-fine-grained leaks may not be detectable on your platform

The outcome includes diagnostic information about precision:

use tacet::Outcome;
match outcome {
Outcome::Pass { diagnostics, .. } | Outcome::Fail { diagnostics, .. } => {
// What you requested
let requested = diagnostics.theta_user;
// What was actually tested
let effective = diagnostics.theta_eff;
if effective > requested {
println!(
"Note: Threshold elevated from {:.1}ns to {:.1}ns",
requested, effective
);
}
}
// ...
}

A ThresholdElevated flag in diagnostics indicates when elevation occurred.