Exposing the LibC time function calls

I’m currently working on a project that requires a time based tick. While Time::Span and time.monotonic fill this need functionally, they both do processing on the data that makes them much (20-30%) slower than they could be for my purpose.

Requiring the correct files for LibC call on a single system is easy, but for cross-platform support it gets messy. In my project I’ve extracted the time functions to expose them directly, but it feels hackish and like something that could easily be supported by Crystal directly. These changes would be beneficial to those wanting a more performant time, to create user-defined time structure, as making any future changes to Crystal’s internal time structures easier.

To that end I suggest modifying
https://github.com/crystal-lang/crystal/blob/master/src/crystal/system/unix/time.cr#L16 and https://github.com/crystal-lang/crystal/blob/master/src/crystal/system/win32/time.cr to have two additional functions, tentatively called real_time and mono_time. These two functions would make the system appropriate calls for real and monotonic time, respectively to C and handle errors.

In turn, compute_utc_seconds_and_nanoseconds and monotonic would call these new functions and process the data as before, leaving their usage unchanged.

Purposed changes to /src/crystal/system/unix/time.cr are below to give a better idea of what I’m talking about. Thoughts?

def self.real_time
  {% if LibC.methods.includes?("clock_gettime".id) %}
    ret = LibC.clock_gettime(LibC::CLOCK_REALTIME, out timespec)
    raise Errno.new("clock_gettime") unless ret == 0
    timespec
  {% else %}
    ret = LibC.gettimeofday(out timeval, nil)
    raise Errno.new("gettimeofday") unless ret == 0
    timeval
  {% end %}
end

def self.mono_time
  {% if flag?(:darwin) %}
    info = mach_timebase_info
    total_nanoseconds = LibC.mach_absolute_time * info.numer // info.denom
  {% else %}
    if LibC.clock_gettime(LibC::CLOCK_MONOTONIC, out tp) == 1
      raise Errno.new("clock_gettime(CLOCK_MONOTONIC)")
    end
    tp
  {% end %}
end

def self.compute_utc_seconds_and_nanoseconds : {Int64, Int32}
  {% if LibC.methods.includes?("clock_gettime".id) %}
    timespec = real_time
    {timespec.tv_sec.to_i64 + UnixEpochInSeconds, timespec.tv_nsec.to_i}
  {% else %}
    timeval = real_time
    {timeval.tv_sec.to_i64 + UnixEpochInSeconds, timeval.tv_usec.to_i * 1_000}
  {% end %}
end

def self.monotonic : {Int64, Int32}
  {% if flag?(:darwin) %}
    total_nanoseconds = mono_time
    seconds = total_nanoseconds // 1_000_000_000
    nanoseconds = total_nanoseconds.remainder(1_000_000_000)
    {seconds.to_i64, nanoseconds.to_i32}
  {% else %}
    tp = mono_time
    {tp.tv_sec.to_i64, tp.tv_nsec.to_i32}
  {% end %}
end

Extracting those method would theoretically be possible, but I don’t think this is a good idea:

  • The entire Crystal::System namespace is not part of the public API and intended for internal use by the stdlib only. It is not supposed to provide anything else than required for the stdlib. And you would be relying on an unofficial API.
  • Those methods would expose platform-specific data types, thus hindering portability. That’s even less incentive to provide such an API in the stdlib, even an unofficial one.
  • compute_utc_seconds_and_nanoseconds and monotonic apply some modification to the OS data, but these are simple math operations which don’t eat many CPU cycles. Thus, the absolute performance overhead is negligible in a work context. I expect the use cases where this would be relevant to be almost nonexistent. (what exactly are you doing that this tiny improvement matter so much?)

It’s probably better to implement these customized methods yourself, by copying the relevant code from the stdlib to your shard.

1 Like