I am NOT posting this as a suggestion for implementation. I just find this
quite interesting, so I am posting it here for anyone who might also find it
interesting. Feel free to cherry-pick some parts, or take the whole thing.
The attached patch makes the following changes to the `time' command:
1) Adds a compile-time choice to use CLOCK_MONOTONIC, if available.
As a result, now we have a good timing accuracy on Cygwin too.
2) If shell-var TIMEWARMUP exists, and if it is set to an integer N>0, then
`time' runs a warmup loop N times, in hope to awaken idled-down CPUs before
timing the command. This can improve the accuracy of the measurement.
The observed benefit of this varies widely from one type of CPU to another.
(Down to no effect whatsoever on some.)
3) If shell-var TIMEREPEAT exists, and if it is set to an integer N>1, then
`time' runs the command N times before taking the final clock-reading.
4) `time' leaves the elapsed time (wall clock) result in shell-var TIMERESULT,
according to the "%[p][l]R" portion of the TIMEFORMAT variable.
5) By moving the "before" and "after" clock-readings as close as possible to
the
command that's being timed, it noticeably increases the accuracy of the results
when measuring some fast, short-lasting commands.
6) Three bugs were fixed. A few other minor bugs are waiting for input from
the developer.
7) Cleaned up some code for which I couldn't find a way to leave it as-is.
I left comments about the necessity of the changes.
8) There's one outstanding problem: If "(posixly_correct && nullcmd)", and if
compiled with CLOCK_MONOTONIC then `time' will display a result equivalent to
`uptime', rather than the time since the shell was started. I'll fix that.
But currently "if (posixly_correct && nullcmd)" is broken anyway, should the
compiler fall back to something other than "defined (HAVE_GETTIMEOFDAY)".
So, these are the vars that this version is using, if they exist...
TIMEFORMAT
TIMEWARMUP
TIMEREPEAT
TIMERESULT
(At this point I can picture Greg Wooledge crying "This is preposterous!",
and his head exploding.:)
Tested on Slackware Linux, and on Alpine Linux.
Patch (apply with `patch -p0'):
This patch makes the following changes to the `time' command:
1) Adds a compile-time choice to use CLOCK_MONOTONIC, if available.
As a result, now we have a good timing accuracy on Cygwin too.
2) If shell-var TIMEWARMUP exists, and if it is set to an integer N>0, then
`time' runs a warmup loop N times, in hope to awaken idled-down CPUs before
timing the command. This can improve the accuracy of the measurement.
The observed benefit of this varies widely from one type of CPU to another.
(Down to no effect whatsoever on some.)
3) If shell-var TIMEREPEAT exists, and if it is set to an integer N>1, then
`time' runs the command N times before taking the final clock-reading.
4) `time' leaves the elapsed time (wall clock) result in shell-var TIMERESULT,
according to the "%[p][l]R" portion of the TIMEFORMAT variable.
5) By moving the "before" and "after" clock-readings as close as possible to
the
command that's being timed, it noticeably increases the accuracy of the results
when measuring some fast, short-lasting commands.
6) Three bugs were fixed. A few other minor bugs are waiting for input from
the developer.
7) Cleaned up some code for which I couldn't find a way to leave it as-is.
I left comments about the necessity of the changes.
8) There's one outstanding problem: If "(posixly_correct && nullcmd)", and if
compiled with CLOCK_MONOTONIC then `time' will display a result equivallent to
`uptime', rather than the time since the shell was started. I'll fix that.
But currently "if (posixly_correct && nullcmd)" is broken anyway, should the
compiler fall back to something other than "defined (HAVE_GETTIMEOFDAY)".
Patch (apply with `patch -p0'):
--- execute_cmd.c Thu Jun 5 10:02:01 2025
+++ ../bash-5.3-patched/builtins/execute_cmd.c Mon Sep 29 04:16:26 2025
@@ -108,8 +108,8 @@
extern int command_string_index;
extern char *the_printed_command;
-extern time_t shell_start_time;
-#if defined (HAVE_GETTIMEOFDAY)
+extern time_t shell_start_time; // <--*** BUG: if there's no
HAVE_GETTIMEOFDAY then there can't be any shell_start_time
+#if defined (HAVE_GETTIMEOFDAY) // ...as shell.c calls
gettimeofday() to put some value in shell_start_time.
extern struct timeval shellstart;
#endif
#if 0
@@ -1235,10 +1235,18 @@
#if defined (COMMAND_TIMING)
-#if defined (HAVE_GETRUSAGE) && defined (HAVE_GETTIMEOFDAY)
+#define TEST_NEW_CLOCK_MONOTONIC
+#define TEST_NEW_TIMEWARMUP
+#define TEST_NEW_TIMEREPEAT
+#define TEST_NEW_TIMERESULT
+
+#if defined (HAVE_GETRUSAGE) || defined (HAVE_GETTIMEOFDAY)
extern struct timeval *difftimeval (struct timeval *, struct timeval *, struct
timeval *);
extern struct timeval *addtimeval (struct timeval *, struct timeval *, struct
timeval *);
-extern int timeval_to_cpu (struct timeval *, struct timeval *, struct timeval
*);
+#endif
+
+#if defined (CLOCK_MONOTONIC)
+extern struct timespec *difftimespec (struct timespec *, struct timespec *,
struct timespec *);
#endif
#define POSIX_TIMEFORMAT "real %2R\nuser %2U\nsys %2S"
@@ -1374,13 +1382,8 @@
else if (s[1] == 'P')
{
s++;
-#if 0
- /* clamp CPU usage at 100% */
- if (cpu > 10000)
- cpu = 10000;
-#endif
sum = cpu / 100;
- sum_frac = (cpu % 100) * 10;
+ sum_frac = cpu % 100 * 10000; // mkfmt expects the fractional part
to be in microseconds!
len = mkfmt (ts, 2, 0, sum, sum_frac);
RESIZE_MALLOCED_BUFFER (str, sindex, len, ssize, 64);
strcpy (str + sindex, ts);
@@ -1402,7 +1405,12 @@
s++;
}
if (*s == 'R' || *s == 'E')
+ {
len = mkfmt (ts, prec, lng, rs, rsf);
+#if defined (TEST_NEW_TIMERESULT)
+ bind_variable ("TIMERESULT", ts, 0);
+#endif /* TEST_NEW_TIMERESULT */
+ }
else if (*s == 'U')
len = mkfmt (ts, prec, lng, us, usf);
else if (*s == 'S')
@@ -1436,57 +1444,93 @@
char *time_format;
volatile procenv_t save_top_level;
volatile int old_subshell, old_flags;
-
-#if defined (HAVE_GETRUSAGE) && defined (HAVE_GETTIMEOFDAY)
- struct timeval real, user, sys;
- struct timeval before, after;
-# if defined (HAVE_STRUCT_TIMEZONE)
- struct timezone dtz; /* posix doesn't define this */
-# endif
- struct rusage selfb, selfa, kidsb, kidsa; /* a = after, b = before */
-#else
-# if defined (HAVE_TIMES)
- clock_t tbefore, tafter, real, user, sys;
- struct tms before, after;
-# endif
-#endif
-
-#if defined (HAVE_GETRUSAGE) && defined (HAVE_GETTIMEOFDAY)
-# if defined (HAVE_STRUCT_TIMEZONE)
- gettimeofday (&before, &dtz);
-# else
- gettimeofday (&before, NULL);
-# endif /* !HAVE_STRUCT_TIMEZONE */
- getrusage (RUSAGE_SELF, &selfb);
- getrusage (RUSAGE_CHILDREN, &kidsb);
-#else
-# if defined (HAVE_TIMES)
- tbefore = times (&before);
-# endif
-#endif
-
- old_subshell = subshell_environment;
- posix_time = command && (command->flags & CMD_TIME_POSIX);
-
- nullcmd = (command == 0) || (command->type == cm_simple &&
command->value.Simple->words == 0 && command->value.Simple->redirects == 0);
- if (posixly_correct && nullcmd)
- {
-#if defined (HAVE_GETRUSAGE)
- selfb.ru_utime.tv_sec = kidsb.ru_utime.tv_sec = selfb.ru_stime.tv_sec =
kidsb.ru_stime.tv_sec = 0;
- selfb.ru_utime.tv_usec = kidsb.ru_utime.tv_usec = selfb.ru_stime.tv_usec
= kidsb.ru_stime.tv_usec = 0;
- before = shellstart;
-#else
- before.tms_utime = before.tms_stime = before.tms_cutime =
before.tms_cstime = 0;
- tbefore = shell_start_time;
-#endif
- }
-
+
+ rs = rsf = us = usf = ss = ssf = cpu = 0;
+
+ old_subshell = subshell_environment;
+ posix_time = command && (command->flags & CMD_TIME_POSIX);
+ nullcmd = (command == 0) || (command->type == cm_simple &&
command->value.Simple->words == 0 && command->value.Simple->redirects == 0);
+
+#if defined (TEST_NEW_TIMEWARMUP)
+ /* If variable TIMEWARMUP exists, and if it is set to an integer N>1,
+ then time_command will first run a warmup loop N times to increase the
+ CPU frequencies. This greatly improves the accuracy of the measurement. */
+ intmax_t nwarmup;
+ char *vwarmup = get_string_value ("TIMEWARMUP");
+ if (vwarmup == 0 || *vwarmup == 0 || legal_number (vwarmup, &nwarmup) == 0
|| nwarmup < 0)
+ nwarmup = 0; // default: no warmup
+ if (! nullcmd)
+ while (nwarmup--) { asm ("PAUSE"); QUIT; } // asm, so the compiler doesn't
"optimize" this out.
+#endif /* TEST_NEW_TIMEWARMUP */
+
+#if defined (TEST_NEW_TIMEREPEAT)
+ /* If variable TIMEREPEAT exists, and if it is set to an integer N>1,
+ then time_command will execute the pipeline N times between the
+ "before" and the "after" time-readings. */
+ intmax_t nrepeat;
+ char *vrepeat = get_string_value ("TIMEREPEAT");
+ if (vrepeat == 0 || *vrepeat == 0 || legal_number (vrepeat, &nrepeat) == 0
|| nrepeat < 1)
+ nrepeat = 1; // default: run the command once.
+#endif /* TEST_NEW_TIMEREPEAT */
+
+#if 0 /* REMOVE!
+ We don't need to involve time-zones in this. When taking "before" and
"after" readings,
+ their difference will be the time-elapsed, regardless of which time-zone
we're in. */
+# if defined (HAVE_STRUCT_TIMEZONE)
+ struct timezone dtz; /* posix doesn't define this */
+# endif
+#endif /* 0 */
+
+/* There are two kinds of readings we need to do here, and we need to keep
these two
+ separate for things to work properly. One is the reading of the wall-clock,
which
+ can be accomplished by either one of clock_gettime(), gettimeofday(), or
times(),
+ in order of preference. The other kind is the reading of the urs/sys times,
which
+ can be accomplished by either getrusage() or times(). In the original code,
+ getrusage() and gettimeofday() were meshed together in a strange
interdependency,
+ and one would not compile without the other, and it was really messy when
trying
+ to add a new wall-clock. By untangling these two kinds of readings,
everything
+ compiles nicely, and was tested to work for any combination of
absence/presence
+ of the aforementioned four functions. It makes it easy to rearrange the
order of
+ the clock-sources, if needed, or to add a new clock-source in the future. */
+
+#if defined (HAVE_GETRUSAGE)
+ struct rusage selfb, selfa, kidsb, kidsa; /* a = after, b = before */
+ getrusage (RUSAGE_SELF, &selfb);
+ getrusage (RUSAGE_CHILDREN, &kidsb);
+#elif defined (HAVE_TIMES)
+# ifndef USING_TIMES
+# define USING_TIMES /* ..as this can be the fallback in two separate
cases. */
+ clock_t t_before, t_after, t_real, t_user, t_sys;
+ struct tms tms_before, tms_after;
+ t_before = times (&tms_before);
+# endif /* USING_TIMES */
+#endif /* HAVE_GETRUSAGE */
+
+#if defined (CLOCK_MONOTONIC)
+ struct timespec ts_before, ts_after, ts_real;
+ clock_gettime (CLOCK_MONOTONIC, &ts_before);
+#elif defined (HAVE_GETTIMEOFDAY)
+ struct timeval tv_before, tv_after, tv_real;
+ gettimeofday (&tv_before, NULL);
+#elif defined (HAVE_TIMES)
+# ifndef USING_TIMES
+# define USING_TIMES
+ clock_t t_before, t_after, t_real, t_user, t_sys;
+ struct tms tms_before, tms_after;
+ t_before = times (&tms_before);
+# endif /* USING_TIMES */
+#endif /* CLOCK_MONOTONIC */
+
+//
########################################################################################
rv = EXECUTION_SUCCESS; /* suppress uninitialized use warnings
*/
old_flags = command->flags;
COPY_PROCENV (top_level, save_top_level);
command->flags &= ~(CMD_TIME_PIPELINE|CMD_TIME_POSIX);
code = setjmp_nosigs (top_level);
if (code == NOT_JUMPED)
+#if defined (TEST_NEW_TIMEREPEAT)
+ while (nrepeat--)
+#endif /* TEST_NEW_TIMEREPEAT */
rv = execute_command_internal (command, asynchronous, pipe_in, pipe_out,
fds_to_close);
COPY_PROCENV (save_top_level, top_level);
@@ -1498,57 +1542,84 @@
original top level. */
if (code != NOT_JUMPED && subshell_environment && subshell_environment !=
old_subshell)
sh_longjmp (top_level, code);
-
- rs = us = ss = 0;
- rsf = usf = ssf = 0;
- cpu = 0;
-
-#if defined (HAVE_GETRUSAGE) && defined (HAVE_GETTIMEOFDAY)
-# if defined (HAVE_STRUCT_TIMEZONE)
- gettimeofday (&after, &dtz);
-# else
- gettimeofday (&after, NULL);
-# endif /* !HAVE_STRUCT_TIMEZONE */
- getrusage (RUSAGE_SELF, &selfa);
- getrusage (RUSAGE_CHILDREN, &kidsa);
-
- difftimeval (&real, &before, &after);
- timeval_to_secs (&real, &rs, &rsf, 1000000);
-
- addtimeval (&user, difftimeval(&after, &selfb.ru_utime, &selfa.ru_utime),
- difftimeval(&before, &kidsb.ru_utime, &kidsa.ru_utime));
- timeval_to_secs (&user, &us, &usf, 1000000);
-
- addtimeval (&sys, difftimeval(&after, &selfb.ru_stime, &selfa.ru_stime),
- difftimeval(&before, &kidsb.ru_stime, &kidsa.ru_stime));
- timeval_to_secs (&sys, &ss, &ssf, 1000000);
-
- cpu = timeval_to_cpu (&real, &user, &sys);
-#else
-# if defined (HAVE_TIMES)
- tafter = times (&after);
-
- real = tafter - tbefore;
- clock_t_to_secs (real, &rs, &rsf);
- /* clock_t_to_secs returns RSF in milliseconds; multipy by 1000 to get
microseconds. */
- rsf *= 1000;
-
- user = (after.tms_utime - before.tms_utime) + (after.tms_cutime -
before.tms_cutime);
- clock_t_to_secs (user, &us, &usf);
- usf *= 1000;
-
- sys = (after.tms_stime - before.tms_stime) + (after.tms_cstime -
before.tms_cstime);
- clock_t_to_secs (sys, &ss, &ssf);
- ssf *= 1000;
-
- cpu = (real == 0) ? 0 : ((user + sys) * 10000) / real;
-
-# else
- rs = us = ss = 0;
- rsf = usf = ssf = cpu = 0;
-# endif
-#endif
-
+//
########################################################################################
+
+ /* Take the "after" wall-clock readings, before anything else. */
+#if defined (CLOCK_MONOTONIC)
+ clock_gettime (CLOCK_MONOTONIC, &ts_after);
+#elif defined (HAVE_GETTIMEOFDAY)
+ gettimeofday (&tv_after, NULL);
+#endif
+
+ /* The proper place for this is here. */
+ if (posixly_correct && nullcmd) {
+#if defined (USING_TIMES)
+ tms_before.tms_utime = tms_before.tms_stime = tms_before.tms_cutime =
tms_before.tms_cstime = 0;
+ t_before = shell_start_time; // ***BUG: This should be in ticks, but
shell_start_time has seconds in it.
+#endif
+#if defined (HAVE_GETRUSAGE)
+ selfb.ru_utime.tv_sec = kidsb.ru_utime.tv_sec = selfb.ru_stime.tv_sec =
kidsb.ru_stime.tv_sec = 0;
+ selfb.ru_utime.tv_usec = kidsb.ru_utime.tv_usec = selfb.ru_stime.tv_usec
= kidsb.ru_stime.tv_usec = 0;
+#endif
+#if defined (CLOCK_MONOTONIC)
+ ts_before.tv_sec = ts_before.tv_nsec = 0; // That's the time the OS
booted, not shell start. ***FIXIT!
+#elif defined (HAVE_GETTIMEOFDAY)
+ tv_before = shellstart; // timeval
+#endif
+ }
+
+ /* It is important that we put this here... */
+#if defined (USING_TIMES)
+ t_after = times (&tms_after);
+
+ t_real = t_after - t_before; // ticks // <-- *** There is a BUG coming
from shell_start_time
+ clock_t_to_secs (t_real, &rs, &rsf);
+ /* clock_t_to_secs returns RSF in milliseconds; multipy by 1000 to get
microseconds. */
+ rsf *= 1000;
+
+ t_user = (tms_after.tms_utime - tms_before.tms_utime) +
(tms_after.tms_cutime - tms_before.tms_cutime);
+ clock_t_to_secs (t_user, &us, &usf);
+ usf *= 1000;
+
+ t_sys = (tms_after.tms_stime - tms_before.tms_stime) + (tms_after.tms_cstime
- tms_before.tms_cstime);
+ clock_t_to_secs (t_sys, &ss, &ssf);
+ ssf *= 1000;
+
+ cpu = (t_real == 0) ? 0 : ((t_user + t_sys) * 10000) / t_real;
+#endif
+
+ // The current version of time_command() needs the seconds fraction in
microseconds. So...
+ int sf_scale = 1000000;
+
+#if defined (HAVE_GETRUSAGE)
+ getrusage (RUSAGE_SELF, &selfa);
+ getrusage (RUSAGE_CHILDREN, &kidsa);
+
+ struct timeval rutv_before, rutv_after, rutv_user, rutv_sys;
+
+ addtimeval (&rutv_user, difftimeval(&rutv_after, &selfb.ru_utime,
&selfa.ru_utime),
+ difftimeval(&rutv_before, &kidsb.ru_utime,
&kidsa.ru_utime));
+ timeval_to_secs (&rutv_user, &us, &usf, sf_scale);
+
+ addtimeval (&rutv_sys, difftimeval(&rutv_after, &selfb.ru_stime,
&selfa.ru_stime),
+ difftimeval(&rutv_before, &kidsb.ru_stime,
&kidsa.ru_stime));
+ timeval_to_secs (&rutv_sys, &ss, &ssf, sf_scale);
+#endif
+
+#if defined (CLOCK_MONOTONIC)
+ difftimespec (&ts_real, &ts_before, &ts_after);
+ timespec_to_secs (&ts_real, &rs, &rsf, sf_scale);
+#elif defined (HAVE_GETTIMEOFDAY)
+ difftimeval (&tv_real, &tv_before, &tv_after);
+ timeval_to_secs (&tv_real, &rs, &rsf, sf_scale);
+#endif
+
+ long real_scaled = rs * sf_scale + rsf;
+ if (real_scaled>0) {
+ long usr_sys_scaled = (us + ss) * sf_scale + usf + ssf;
+ cpu = usr_sys_scaled * 10000 / real_scaled; // *100 for percent, then
another *100 to keep 2 fract digits.
+ }
+
if (posix_time)
time_format = POSIX_TIMEFORMAT;
else if ((time_format = get_string_value ("TIMEFORMAT")) == 0)
--- lib/sh/timeval.c Thu Jun 15 16:02:53 2023
+++ ../bash-5.3-patched/builtins/lib/sh/timeval.c Mon Sep 29 04:30:30 2025
@@ -52,6 +52,24 @@
return d;
}
+struct timespec *
+difftimespec (struct timespec *d, struct timespec *t1, struct timespec *t2)
+{
+ d->tv_sec = t2->tv_sec - t1->tv_sec;
+ d->tv_nsec = t2->tv_nsec - t1->tv_nsec;
+ if (d->tv_nsec < 0)
+ {
+ d->tv_nsec += 1000000000;
+ d->tv_sec -= 1;
+ if (d->tv_sec < 0) /* ??? -- BSD/OS does this */
+ {
+ d->tv_sec = 0;
+ d->tv_nsec = 0;
+ }
+ }
+ return d;
+}
+
struct timeval *
addtimeval (struct timeval *d, struct timeval *t1, struct timeval *t2)
{
@@ -123,33 +141,45 @@
return ((t2.tv_sec == 0) ? 0 : t1.tv_sec / t2.tv_sec);
}
-/* Convert a pointer to a struct timeval to seconds and fractions of a
- second, returning the values in *SP and *SFP, respectively. The precision
- of the fractional part is determined by MAXVAL. For instance, if MAXVAL
- is 10000000, this just returns the tv_usec field. This does rounding on
- the fractional part, not just truncation to three places. */
-void
-timeval_to_secs (struct timeval *tvp, time_t *sp, long *sfp, int maxval)
-{
- int rest;
-
- *sp = tvp->tv_sec;
-
- *sfp = tvp->tv_usec % 1000000; /* pretty much a no-op */
- if (maxval < 1000000) /* don't bother otherwise */
- {
- rest = *sfp % maxval;
- *sfp = (*sfp * maxval) / 1000000;
- if (rest >= maxval/2)
- *sfp += 1;
- }
+/* Convert a pointer to a struct timeval to seconds and fractions
+ of a second, returning the values in *SP and *SFP, respectively.
+ The precision of the fractional part is determined by frac_scale.
+ Properly rounds the fractional part.
+
+ NOTE: This function was assumed to work only with microseconds, as noted in
+ execute_cmd.c. Yes, timeval values have their fractional part in
microseconds,
+ but this function returns their fractional part in whatever unit-scale is
+ requested by frac_scale -- be it centiseconds, nanoseconds, what have you.
*/
+void timeval_to_secs (struct timeval *tvp, time_t *sp, long *sfp, long
frac_scale) {
+ long mul, div, base_scale = 1000000;
+ *sp = tvp->tv_sec;
+ *sfp = tvp->tv_usec;
+
+ if (frac_scale < base_scale) {
+ div = base_scale / frac_scale;
+ *sfp = ( *sfp * 10 / div + 5) / 10;
+ }
+
+ if (frac_scale > base_scale) {
+ mul = frac_scale / base_scale;
+ *sfp = *sfp * mul;
+ }
+}
- /* Sanity check */
- if (*sfp >= maxval)
- {
- *sp += 1;
- *sfp -= maxval;
- }
+void timespec_to_secs (struct timespec *tsp, time_t *sp, long *sfp, long
frac_scale) {
+ long mul, div, base_scale = 1000000000;
+ *sp = tsp->tv_sec;
+ *sfp = tsp->tv_nsec;
+
+ if (frac_scale < base_scale) {
+ div = base_scale / frac_scale;
+ *sfp = ( *sfp * 10 / div + 5) / 10;
+ }
+
+ if (frac_scale > base_scale) {
+ mul = frac_scale / base_scale;
+ *sfp = *sfp * mul;
+ }
}
/* Print the contents of a struct timeval * in a standard way to stdio
--- externs.h Fri Mar 7 17:48:53 2025
+++ ../bash-5.3-patched/builtins/externs.h Mon Sep 29 03:43:26 2025
@@ -500,7 +500,8 @@
so we don't have to count on having a definition of struct timeval in
scope when this file is included. */
#ifdef NEED_TIMEVAL_FUNCS_DECL
-extern void timeval_to_secs (struct timeval *, time_t *, long *, int);
+extern void timeval_to_secs (struct timeval *, time_t *, long *, long);
+extern void timespec_to_secs (struct timespec *, time_t *, long *, long);
extern void print_timeval (FILE *, struct timeval *);
#endif
--