Tags

, , , , , , , ,

A Question at Stack Overflow asked: “How can I write a script to calculate the time the script took to complete?”

This article will use that question (and my answer to it) as a jumping off point to discuss some ways to measure elapsed time at a Windows command prompt. (Note that while the question used the word “script”, from context they are asking about Windows and the command prompt and clearly meant .BAT file as interpreted by CMD.EXE.)

Naturally, if you are willing to use tools that are not part of a stock Windows distribution this becomes easy. Unix has the time(1) utility that runs a program and reports how long it took, and GNU Linux includes /usr/bin/time (or at least Ubuntu does) even today. If you have a port of GNU date (which comes in the GNU coreutils package) handy, you could use it with the +%s format before and after and subtract to get the elapsed time in seconds. There are certainly other timing and benchmarking tools that could be used.

But the challenge is to do it using only capabilities built in to Windows.

Any old hand at DOS will quickly dismiss the idea as impractical, since the BAT language didn’t really include any features designed to do arithmetic or nicely formatted output. However, when Windows 9x got replaced by the “new technology” of the Windows NT kernel, it brought along a complete rewrite of the command prompt and BAT language that quietly added a lot of features. Among them is the ability to capture a time stamp and to do arithmetic.

In short, the batch language is a lot more powerful than it is given credit for.

Proving that point, here is a simple implementation of TIMER.BAT that I posted as my answer to the Question. The version posted here owes some credit to the sharp eyes of other SO users.

    @echo off
    setlocal

    rem Remember start time. Note that we don't look at the date, so this
    rem calculation won't work right if the program run spans local midnight.
    set t0=%time: =0%

    rem do something here.... but probably with more care about quoting.
    rem specifically, odd things will happen if any arguments contain 
    rem percent signs or carets or other characters special to the 
    rem CMD.EXE command line and there may be no way to prevent it.
    %*

    rem Capture the end time before doing anything else
    set t=%time: =0%

    rem make t0 into a scaler in 100ths of a second, being careful not 
    rem to let SET/A misinterpret 08 and 09 as octal
    set /a h=1%t0:~0,2%-100
    set /a m=1%t0:~3,2%-100
    set /a s=1%t0:~6,2%-100
    set /a c=1%t0:~9,2%-100
    set /a starttime = %h% * 360000 + %m% * 6000 + 100 * %s% + %c%

    rem make t into a scaler in 100ths of a second
    set /a h=1%t:~0,2%-100
    set /a m=1%t:~3,2%-100
    set /a s=1%t:~6,2%-100
    set /a c=1%t:~9,2%-100
    set /a endtime = %h% * 360000 + %m% * 6000 + 100 * %s% + %c%

    rem runtime in 100ths is now just end - start
    set /a runtime = %endtime% - %starttime%
    set runtime = %s%.%c%

    echo Started at %t0%
    echo Ran for %runtime%0 ms

You could simplify the arithmetic and be a little more honest about the overall accuracy of this by not bothering with the 100ths of a second part. Here it is in action, assuming you have a sleep command or some other time waster:

C:... TIMER SLEEP 3
Script took 3000 ms to complete
C:... 

Due to a quirk of the way batch files are invoked by an outer batch file, you must include the keyword CALL if the command you are timing is itself implemented by a BAT file:

C:... TIMER CALL LONGJOB.BAT
Script took 185230 ms to complete
C:...  

Without the keyword CALL, the inner BAT file will be executed, but control will never return to TIMER.BAT and it won’t be able to finish the time computation and print the result.

TIMER.BAT demonstrates several techniques new to CMD.EXE.

One of those is SETLOCAL which prevents variables from modifying the caller’s environment. Any serious BAT file should almost certainly use SETLOCAL unless the intent really is to modify environment variables.

Another is SET /A which gives you a remarkable amount of arithmetic. The principle trick I’ve used here is the new substring extraction syntax where %t:~3,2% means the two characters starting at offset 3 in the value of the variable named t.

For a real shocker, take a look at the full description of set (try SET /? at a prompt) and if that doesn’t scare you, look at FOR /? and notice that it can parse text out of files…

One gotcha that went unnoticed for a long time was the result of executing this during the 8th or 9th second of a minute, or the 8th or 9th minute of an hour or the 8th or 9th hour of the day. In those cases, the conversion from ASCII text to a numeric value apparently treated any character sequence beginning with “0” (but not “0x”) as an octal number and octal does not have digits valued 8 or 9. We settled on inserting a leading “1” before the value, then subtracting the extra 100 from it after it was safely converted.

Note that there is a glaring oversight here that I’m probably not going to fix. It won’t work if the command starts on a different day than it ends. That is, it will do some math related to the time of day and report a difference, but the difference will be wrong by 86400 seconds per day spanned. I’ve left fixing that up as an issue for any readers who might care.

Fixing it to at least warn about this case is easy. Fixing it to do the right thing is harder.

The last gotcha I’ll mention was the observation that %time% actually represents a single digit hour with a leading space character. I fixed that with the new string substitution syntax %time: =0% which replaces the space with a leading zero, then falling to the leading 1 and -100 trick.

So there you have it. Using only features built in to CMD.EXE in a stock version of Windows (last tested on Windows 7 Pro 64-bit) it is possible to measure the elapsed time with a batch file.

Advertisements