Skip to content

Commit

Permalink
Merge pull request #9 from savi-lang/add/time-measure
Browse files Browse the repository at this point in the history
Add `Time.Measure` functions leveraging system monotonic clocks.
  • Loading branch information
jemc authored Mar 18, 2022
2 parents 404046d + a70902e commit 1bc5581
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 0 deletions.
1 change: 1 addition & 0 deletions spec/Main.savi
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
Spec.Process.run(env, [
Spec.Run(Time.Spec).new(env)
Spec.Run(Time.Formatter.Spec).new(env)
Spec.Run(Time.Measure.Spec).new(env)
])
53 changes: 53 additions & 0 deletions spec/Time.Measure.Spec.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
:class Time.Measure.Spec
:is Spec
:const describes: "Time.Measure"

:it "measures nanoseconds spent in the given block"
run_count = 0
prior = Time.Measure.current_nanoseconds
measured = Time.Measure.nanoseconds -> (
run_count += 1
_FFI.TestHacks.sleep(1)
)
after = Time.Measure.current_nanoseconds

assert: run_count == 1
assert: measured >= 1000000000 // at least 1 second (due to the sleep)
assert: measured <= 10000000000 // it should never take more than 10 seconds
assert: measured <= (after - prior)

:it "measures microseconds spent in the given block"
run_count = 0
prior = Time.Measure.current_microseconds
measured = Time.Measure.microseconds -> (
run_count += 1
_FFI.TestHacks.sleep(1)
)
after = Time.Measure.current_microseconds

assert: run_count == 1
assert: measured >= 1000000 // at least 1 second (due to the sleep)
assert: measured <= 10000000 // it should never take more than 10 seconds
assert: measured <= (after - prior)

:it "measures milliseconds spent in the given block"
run_count = 0
prior = Time.Measure.current_milliseconds
measured = Time.Measure.milliseconds -> (
run_count += 1
_FFI.TestHacks.sleep(1)
)
after = Time.Measure.current_milliseconds

assert: run_count == 1
assert: measured > 0
assert: measured >= 1000 // at least 1 second (due to the sleep)
assert: measured <= 10000 // it should never take more than 10 seconds
assert: measured <= (after - prior)

// It's never advisable to use `sleep` in Savi, given that it blocks the
// entire scheduler thread from making progress. However, as a test hack
// it's reasonable because it helps us test a specific non-zero amount of
// time passing inside the block we are measuring.
:module _FFI.TestHacks
:ffi sleep(seconds I32) I32
149 changes: 149 additions & 0 deletions src/Time.Measure.savi
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
:: A collection of functions relating to measuring intervals of time elapsed
:: during some portion of the program's execution.
:module Time.Measure
:: Measure the interval in nanoseconds that elapse in the given yield block.
:fun nanoseconds
:yields None for None
prior = @current_nanoseconds
yield None
@current_nanoseconds - prior

:: Measure the interval in microseconds that elapse in the given yield block.
:fun microseconds
:yields None for None
prior = @current_microseconds
yield None
@current_microseconds - prior

:: Measure the interval in milliseconds that elapse in the given yield block.
:fun milliseconds
:yields None for None
prior = @current_milliseconds
yield None
@current_milliseconds - prior

:: Get the current monotonic non-adjusted nanosecond count.
::
:: This number is not guaranteed to have any particular relationship to the
:: current wall clock time - it is more likely to be related to system uptime,
:: although there is no particular guarantee of that relationship either.
:: For wall clock time, call `Time.now` instead.
::
:: Being monotonic and non-adjusted, this is suitable for calculating
:: time intervals using subtraction with wrap-around underflow semantics.
:: That is, because the underlying count will wrap around on overflow,
:: subtraction of two values across time should wrap around on underflow
:: to properly account for the case of two values that straddle an overflow.
:: See `nanoseconds` function for an example of how to measure time.
::
:: This function is intended to be used for asynchronous timing measurements.
:: For synchronous timing measurements, use the `nanoseconds` function,
:: which handles the timing measurement for you around the given yield block.
:fun current_nanoseconds U64
case (
| Platform.is_windows |
0 // TODO: Windows support
| Platform.is_macos |
// MacOS gives us a count of total uptime "ticks" since the OS booted.
// We don't know how long a "tick" is without asking the operating system,
// so we ask it for the timebase and multiple/divide to get nanoseconds.
timebase = Pair(U32, U32).new(0, 0)
timebase_res = _FFI.mach_timebase_info(stack_address_of_variable timebase)
return 0 if (timebase_res != 0)
_FFI.mach_absolute_time
* timebase.first.u64
/ timebase.last.u64
|
// Other POSIX platforms give us the seconds since unix epoch and
// nanoseconds as a pair, which we combine. We choose the monotonic clock.
pair = Pair(USize, USize).new(0, 0)
_FFI.clock_gettime(
_FFI.ClockType.monotonic
stack_address_of_variable pair
)
pair.first.u64 * 1000000000 + pair.last.u64
)

:: Get the current monotonic non-adjusted microsecond count.
::
:: This number is not guaranteed to have any particular relationship to the
:: current wall clock time - it is more likely to be related to system uptime,
:: although there is no particular guarantee of that relationship either.
:: For wall clock time, call `Time.now` instead.
::
:: Being monotonic and non-adjusted, this is suitable for calculating
:: time intervals using subtraction with wrap-around underflow semantics.
:: That is, because the underlying count will wrap around on overflow,
:: subtraction of two values across time should wrap around on underflow
:: to properly account for the case of two values that straddle an overflow.
:: See `microseconds` function for an example of how to measure time.
::
:: This function is intended to be used for asynchronous timing measurements.
:: For synchronous timing measurements, use the `microseconds` function,
:: which handles the timing measurement for you around the given yield block.
:fun current_microseconds U64
case (
| Platform.is_windows |
0 // TODO: Windows support
| Platform.is_macos |
// MacOS gives us a count of total uptime "ticks" since the OS booted.
// We don't know how long a "tick" is without asking the operating system,
// so we ask it for the timebase and multiple/divide to get microseconds.
timebase = Pair(U32, U32).new(0, 0)
timebase_res = _FFI.mach_timebase_info(stack_address_of_variable timebase)
return 0 if (timebase_res != 0)
_FFI.mach_absolute_time
* timebase.first.u64
/ (timebase.last.u64 * 1000)
|
// Other POSIX platforms give us the seconds since unix epoch and
// nanoseconds as a pair, which we combine. We choose the monotonic clock.
pair = Pair(USize, USize).new(0, 0)
_FFI.clock_gettime(
_FFI.ClockType.monotonic
stack_address_of_variable pair
)
pair.first.u64 * 1000000 + pair.last.u64 / 1000
)

:: Get the current monotonic non-adjusted millisecond count.
::
:: This number is not guaranteed to have any particular relationship to the
:: current wall clock time - it is more likely to be related to system uptime,
:: although there is no particular guarantee of that relationship either.
:: For wall clock time, call `Time.now` instead.
::
:: Being monotonic and non-adjusted, this is suitable for calculating
:: time intervals using subtraction with wrap-around underflow semantics.
:: That is, because the underlying count will wrap around on overflow,
:: subtraction of two values across time should wrap around on underflow
:: to properly account for the case of two values that straddle an overflow.
:: See `milliseconds` function for an example of how to measure time.
::
:: This function is intended to be used for asynchronous timing measurements.
:: For synchronous timing measurements, use the `milliseconds` function,
:: which handles the timing measurement for you around the given yield block.
:fun current_milliseconds U64
case (
| Platform.is_windows |
0 // TODO: Windows support
| Platform.is_macos |
// MacOS gives us a count of total uptime "ticks" since the OS booted.
// We don't know how long a "tick" is without asking the operating system,
// so we ask it for the timebase and multiple/divide to get milliseconds.
timebase = Pair(U32, U32).new(0, 0)
timebase_res = _FFI.mach_timebase_info(stack_address_of_variable timebase)
return 0 if (timebase_res != 0)
_FFI.mach_absolute_time
* timebase.first.u64
/ (timebase.last.u64 * 1000000)
|
// Other POSIX platforms give us the seconds since unix epoch and
// nanoseconds as a pair, which we combine. We choose the monotonic clock.
pair = Pair(USize, USize).new(0, 0)
_FFI.clock_gettime(
_FFI.ClockType.monotonic
stack_address_of_variable pair
)
pair.first.u64 * 1000 + pair.last.u64 / 1000000
)
4 changes: 4 additions & 0 deletions src/Time.savi
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@
Time.Formatter.new(pattern).format(@)

:: Get the current wall-clock adjusted system time (in UTC).
::
:: Because this time is not monotonic and gets adjusted to match wall clocks,
:: it is not suitable for measuring intervals of time during the program.
:: To measure time intervals, use functions of `Time.Measure` instead.
:new now
case (
| Platform.is_windows |
Expand Down
7 changes: 7 additions & 0 deletions src/_FFI.savi
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
time_pair_out CPointer(Pair(USize, USize))
) I32

:: C function used to get the system uptime ticks on MacOS.
:fun mach_absolute_time U64

:: C function used to get the numerator and denominator for ticks on MacOS.
:fun mach_timebase_info(info CPointer(Pair(U32, U32))) I32

:: Clock IDs to use when calling `_FFI.clock_gettime`.
:module _FFI.ClockType
:fun real_time U32: 0
:fun monotonic U32: if Platform.is_linux (1 | 4)

0 comments on commit 1bc5581

Please sign in to comment.