Swift-system-clock: low-level no-overhead clock for Swift

I've just tagged v1.0.0-beta.1 of GitHub - swift-dns/swift-system-clock: Low-level no-overhead system clock for Swift · GitHub.

This package is helpful for the users who have needs that stdlib's Continuous/SuspendingClocks don't satisfy.

Examples:

  • Sometimes a mere continuous clock is too expensive to call and you'd like to trade accuracy for speed. Maybe you're just stamping a log-line. In that case you can use a continuousCoarse clock.
  • Or maybe you'd like to count the time your process has been running on CPU cores, even if it means the clock calls can be much more expensive.

Example usage:

import SystemClock

let clock = SystemClock.processUserTime
let elapsed: CompactDuration = clock.measure {
    expensiveWork()
}

let now = SystemClock.realtime.now

This post is an intro for swift-system-clock. For more info see the library's README.

Goals

Swift Interface to use any OS-provided clock

Swift's own Clocks only support "continuous" or "suspending", while you can access "walltime" clocks via other means as well.

To achieve this goal, there are interfaces so you can either use the predefined clocks:

/// Currently one of: realtime, realtimeCoarse, continuous, continuousCoarse,
/// suspending, suspendingCoarse, processCPUTime, threadCPUTime
/// processUserTime, threadUserTime, processSystemTime, threadSystemTime
let myClock = SystemClock.continuousCoarse

Or if you prefer the clock to talk with Swift.Duration instead of the library's CompactDuration:

let myClock = GenericSystemClock<Swift.Duration>.continuousCoarse

Easy to use, even while multiple platforms are supported

In swift-system-clock, you can simply declare a SystemClock and forget about different platforms. It'll "just work" everywhere as well as the platform supports.

If a power user requires so, they can manually specify what clocks to use on each platform:

let processCPUTimeClock = SystemClock(
    darwin: .processCPUTime,
    linux: .processCPUTime,
    windows: .processTime,
    freebsd: .processCPUTime,
    openbsd: .processCPUTime,
    wasi: .monotonic,
    fallback: .monotonic
)
  • Every Apple platform takes darwin. Android takes linux.
  • Unidentified platforms or platforms with no libc will take fallback which uses std::chrono clocks. Just like stdlib does.

Most of the code related to a platform is only compiled when targeting that specific platform. So while the library provides the flexibility, it comes at almost no cost.

Needs to actually work

In short, Codecov shows 90+% test coverage.
There is CI for each platform of Darwin (Apple platforms), Linux (Including Android), Windows, FreeBSD, WASI, and more.
OpenBSD support is not tested as Swift has yet to support OpenBSD.

No overhead

There are benchmarks to confirm the library imposes almost no overhead compared to just manually calling the C APIs.
Furthermore, most of the code related to a platform is only compiled when targeting that specific platform.

The following is the comparison against Swift's own stdlib primitives.
More info in the performance section of the README.

Against Darwin

Benchmark SystemClock (ns/op) Standard Library (ns/op) Speedup
continuous.now 10.6 ns 24.9 ns 2.35x
suspending.now 10.8 ns 24.3 ns 2.25x

Against glibc

Benchmark SystemClock (ns/op) Standard Library (ns/op) Speedup
continuous.now 27.5 ns 29.8 ns 1.08x
suspending.now 27.5 ns 29.5 ns 1.07x

Make it easier to decide on a clock

Last but not least, each clock-id on each platform describes the behavior of the clock.
This is to enable you to more-easily choose the appropriate clock for the task at hand.

Of course clocks can vary not only platform-by-platform, but also artchitecture-by-architecture, and CPU-by-CPU, so optimally you should also do your own measurements.

However the "property tables" like the followings, are meant to ease the decision without having to consult man pages which are sometimes vague, and sometimes miss the info.

All these info were measured on actual hardware.

Where did you measure these?
  • macOS was measured on an M1 Pro (arm64 Apple silicon) under macOS 27, bare metal.
  • Windows on an Intel Core i7-10750H (x86_64) under Windows 11, bare metal.
  • Linux on Ubuntu 24.04 (kernel 6.8, HZ=1000) in a dedicated-core AMD EPYC Milan x86_64 VM from Hetzner.
  • FreeBSD 15.1 and OpenBSD 7.9 in arm64 QEMU virtual machines on the same Mac, where kern.hz is 100.
  • WASI on the same Mac under wasmtime 48 (primary source), Node 26 (uvwasi), Bun 1.3 and WasmKit 0.1.6.

Examples:

Darwin's realtime (CLOCK_REALTIME):

Measures Wall time, counted from 1970-01-01 UTC

Property Value
Reacts to OS time changes :cross_mark: Yes
Reacts to NTP changes :cross_mark: Yes
Counts system suspension times :cross_mark: Yes
Advances while thread is de-scheduled :cross_mark: Yes
Might appear to go backwards :cross_mark: Yes
Reads a cached value :white_check_mark: No
Max staleness :white_check_mark: None
Warm read cost ~ 12ns @ 4GHz
Cold read cost ~ 200ns @ 4GHz
Step granularity 1µs

Linux's boottime (CLOCK_BOOTTIME)

Measures Elapsed time, since the machine booted

Property Value
Reacts to OS time changes :white_check_mark: No
Reacts to NTP changes :cross_mark: Yes
Counts system suspension times :cross_mark: Yes
Advances while thread is de-scheduled :cross_mark: Yes
Might appear to go backwards :white_check_mark: No
Reads a cached value :white_check_mark: No
Max staleness :white_check_mark: None
Warm read cost ~ 25ns @ 4GHz
Cold read cost ~ 11.2µs @ 4GHz
Step granularity 20ns

Linux's processUserTime (RUSAGE_SELF)

Measures CPU time this process spent running its own code

Property Value
Reacts to OS time changes :white_check_mark: No
Reacts to NTP changes :white_check_mark: No
Counts system suspension times :white_check_mark: No
Advances while thread is de-scheduled :white_check_mark: No
Might appear to go backwards :white_check_mark: No
Reads a cached value :white_check_mark: No
Max staleness :cross_mark: ~ 1ms @ HZ 1000
Warm read cost ~ 220ns + ~ 12ns/thread @ 4GHz
Cold read cost ~ 4µs @ 4GHz
Step granularity 1µs

Implementation

Underneath, this library simply calls clock_gettime(2) libc functions, and in few cases some other functions such as getrusage(2).

SystemClock is defined as:

typealias SystemClock = GenericSystemClock<CompactDuration>

CompactDuration is similar to Swift.Duration but it only counts nanoseconds, as there are little usecases for a higher-than-nanoseconds-precision clock in practice.
Currently no clock actually provides values with higher precision than just nanoseconds..

Generally speaking, this library has similar behavior to stdlib.
For example SystemClock.continuous/suspending behave pretty similar to stdlib's counterparts on all platforms.
Or if you're on an unknown platform, this library will fall back to std::chrono, just like stdlib does.

Personal Motivation

My biggest personal motivation is in benchmarking.

While a couple clocks are most what benchmarks need, sometimes you need to specify what exact clock makes sense in your specific case.

With swift-system-clock, a package like ordo-one/benchmark can be extended to simply take any clock you want, and measure via that. To be clear it doesn't need to make things any more difficult and confusing for non-power-users and the APIs can reflect that.

I currently maintain a (personal) fork of ordo-one/benchmark with some extra features and fixes on top since I use it a lot and felt the need for those features, some of which save me hours of time each week.
I'm considering reworking my fork into a more-robust benchmarking toolkit. We'll see.

3 Likes