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
continuousclock 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 acontinuousCoarseclock. - 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 takeslinux. - Unidentified platforms or platforms with no libc will take
fallbackwhich usesstd::chronoclocks. 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?
macOSwas measured on an M1 Pro (arm64 Apple silicon) under macOS 27, bare metal.Windowson an Intel Core i7-10750H (x86_64) under Windows 11, bare metal.Linuxon Ubuntu 24.04 (kernel 6.8,HZ=1000) in a dedicated-core AMD EPYC Milan x86_64 VM from Hetzner.FreeBSD15.1 andOpenBSD7.9 in arm64 QEMU virtual machines on the same Mac, wherekern.hzis 100.WASIon 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 Yes
Reacts to NTP changes Yes
Counts system suspension times Yes
Advances while thread is de-scheduled Yes
Might appear to go backwards Yes
Reads a cached value No
Max staleness 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 No
Reacts to NTP changes Yes
Counts system suspension times Yes
Advances while thread is de-scheduled Yes
Might appear to go backwards No
Reads a cached value No
Max staleness 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 No
Reacts to NTP changes No
Counts system suspension times No
Advances while thread is de-scheduled No
Might appear to go backwards No
Reads a cached value No
Max staleness ~ 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.