Skip to content

Simplify $&time and increase its flexibility #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

jpco
Copy link
Collaborator

@jpco jpco commented May 4, 2025

This PR changes the $&time primitive and the time function. It fixes #154.

Previously, the basic steps of $&time were: (1) measure real time and usage times; (2) run the given command; (3) measure real times and usage again; (4) take the difference and print the result.

We now shrink the scope of $&time so that takes an optional previous set of times as arguments, and it (1) measures the real and usage times, (2) subtracts the argument-times if given, and (3) returns the result. Like this:

; (str times) = <=$&time
; echo $str    # a pre-formatted version for printing
     3.210r     0.005u     0.001s
; echo $times  # raw microsecond-valued real-, user-, and sys-time measurements
3210345 5317 1266
; (str times) = <={$&time $times}
; echo $str
     1.245r     0.000u     0.000s
; echo $times
1245379 478 320

The built-in time function is constructed from this new $&time as:

fn time cmd {
	$&collect                     # for timing consistency; the old `$&time` did this
	let ((str times) = <=$&time)  # first measurement
	unwind-protect {              # protect against exceptions AND use $cmd's return value
		$cmd                  # run the command
	} {
		(str times) = <={$&time $times}  # second measurement, subtracting the first
		echo >[1=2] $str^\t^$^cmd        # print the output and the command
	}
}

The first major benefit of this es-written time is that it is forkless: time {a = b}; echo $a actually does the right thing now, because time doesn't need to fork off a child process.

The second major benefit of this es-written time is that it's flexible -- if you really want a time that forks, you can simply do that (just make sure to do your timing measurements within the child process, or you'll add the overhead of fork to your timing!)

But wait, there's more --- The reduced-scope $&time (and some code cleanup to go along with it) makes it easier to choose among a few different methods of gathering timing information, so we do so, and now we have significantly more precise real-time data on systems that support it.

This version of $&time also makes custom time formatting possible. A motivated user can feed the raw microsecond output of $&time to printf, or awk, or whatever, to report the time the way they want. Or maybe just look at the real-time value and throw the usage values out. Or, one could use the timing data other ways...

This $&time also enables new timing-related use cases. For one thing, a raw let ((s _) = <=$&time) echo $s approximates the times command in other shells. For another, we can do more complex timing setups, like

let ((_ start) = <=$&time)
for (i = <=generate-inputs) {
   process $i
   let ((new _) = <={$&time $start}) echo $new^\t^'after '^$i
}

We can also do some, err, mild hackery to measure "has it been more than $X?" This can be used for things like

fn timeout-after ms cmd {
  let (result = (); (_ start) = <={$&time -$ms^000 0 0})  # get the time $ms in the future
  forever {
    let ((_ acc) = <={$&time $start}) if {!~ $acc(1) -*} {  # test whether we're in the future yet
      return $result
    }
    result = <=$cmd
  }
}
timeout-after 4500 {echo looping; sleep 0.`{shuf -i 1-1000 -n 1}}

This could in theory be done with date +%s, but that would potentially add a lot of fork-exec overhead to gather and do math on all the timestamps. This way also works around es' lack of general arithmetic support, and supports quite a bit of precision.

Limitations of this setup:

Across es invocations, results get wacky with all the measurements. Across processes (like fork {}) results get wacky with usage times specifically.

; local ((_ t) = <=$&time) ./es -c 'let ((_ t) = <={$&time $t}) echo $t'
-38480695 -22102 -14995
; let ((_ t) = <=$&time) fork {let ((_ t) = <={$&time $t}) echo $t}
454 -27563 -24990

Formatting of negative times is broken. I would like to fix this before merging.

; ./es -c 'let ((s _) = <={$&time 1000000 0 0}) echo $s'
    0.-998r     0.001u     0.000s

jpco added 4 commits April 28, 2025 08:22
 - Clean up time code to be much more legible
 - Use intmax_t for timestamps to be more overflow-resistant
 - Add fallback for time functions
 - Add error handling
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

time could be improved
1 participant