delay a batch file in under a second?

Some time ago I posted a method that gives precise timing with delay intervals from 15 milliseconds on. This is a copy of the entire post.


I think I achieved a milliseconds delay with precise timing when the delay is small. I used an hybrid Batch-JScript solution with WScript.Sleep method, but in order to avoid the load delay of the JScript section each time it is used, both parts must be active at same time. The JScript process take the delay in milliseconds, do the delay and send a signal to the Batch section. The Batch process send the number of milliseconds to JScript and wait for the signal. The way to achieve this bi-directional communication is via JScript's WshShwll.Exec method that have access to Batch process' Stdin and Stdout streams.

@if (@CodeSection == @Batch) @then


@echo off
setlocal EnableDelayedExpansion
if defined restart goto secondPart

rem First part: execute JScript section, so it re-execute this Batch file
set restart=true
CScript //nologo //E:JScript "%~F0" "%~F0"
goto :EOF

:secondPart

rem To do a delay, use: "echo #millisecs" followed by "set /P ="; use "echo 0" to end
rem To display data in the screen, use:  echo data > CON
rem To read data from keyboard, use set /P "data=Prompt: " < CON > CON

set runs=10
For %%t in (5 10 15 20 30 50 100 250 500 1000) do (

   set time_idle_ms=%%t
   (
   set t0=!time!
   for /L %%p in (1,1,%runs%) do echo %%t& set /P =
   set t1=!time!
   )

   for /F "tokens=1-8 delims=:.," %%a in ("!t0: =0!:!t1: =0!") do (
      set /a "a=(((1%%e-1%%a)*60)+1%%f-1%%b)*6000+1%%g%%h-1%%c%%d, a+=(a>>31) & 8640000"
   )

   set /a average_time=a*10/runs
   echo(Input:!time_idle_ms! ms - Output: Average time !average_time! ms > CON
)

rem Send the signal to end JScript section
echo 0
goto :EOF


@end


// JScript section

// Restart this Batch file with access to its Stdin and Stdout streams
var WshShell = new ActiveXObject("WScript.Shell");
var BatchFile = WshShell.Exec('"'+WScript.Arguments(0)+'"'), delay;

// Get delay, wait and send CR until delay equ 0
while ((delay = BatchFile.Stdout.ReadLine()) != "0" ) {
   WScript.Sleep(delay);
   BatchFile.Stdin.WriteLine();
}

Output:

Input:5 ms - Output: Average time 15 ms
Input:10 ms - Output: Average time 16 ms
Input:15 ms - Output: Average time 15 ms
Input:20 ms - Output: Average time 32 ms
Input:30 ms - Output: Average time 31 ms
Input:50 ms - Output: Average time 63 ms
Input:100 ms - Output: Average time 109 ms
Input:250 ms - Output: Average time 250 ms
Input:500 ms - Output: Average time 500 ms
Input:1000 ms - Output: Average time 1000 ms

Another test in Windows 8.1 32 bit - 3.2 GHz

Input:5 ms - Output: Average time 14 ms
Input:10 ms - Output: Average time 16 ms
Input:15 ms - Output: Average time 15 ms
Input:20 ms - Output: Average time 31 ms
Input:30 ms - Output: Average time 32 ms
Input:50 ms - Output: Average time 61 ms
Input:100 ms - Output: Average time 110 ms
Input:250 ms - Output: Average time 250 ms
Input:500 ms - Output: Average time 501 ms
Input:1000 ms - Output: Average time 1000 ms

EDIT: pathping test added

Just to complete this topic, I did a timing test using pathping and the same code used to test my method. Here it is:

@echo off
setlocal EnableDelayedExpansion

set runs=10
For %%t in (5 10 15 20 30 50 100 250 500 1000) do (

   set time_idle_ms=%%t
   (
   set t0=!time!
   for /L %%p in (1,1,%runs%) do pathping 127.0.0.1 -n -q 1 -p %%t >nul
   set t1=!time!
   )

   for /F "tokens=1-8 delims=:.," %%a in ("!t0: =0!:!t1: =0!") do (
      set /a "a=(((1%%e-1%%a)*60)+1%%f-1%%b)*6000+1%%g%%h-1%%c%%d, a+=(a>>31) & 8640000"
   )

   set /a average_time=a*10/runs
   echo(Input:!time_idle_ms! ms - Output: Average time !average_time! ms

)

The result show that pathping is not reliable for small delay times:

Input:5 ms - Output: Average time 48 ms
Input:10 ms - Output: Average time 47 ms
Input:15 ms - Output: Average time 47 ms
Input:20 ms - Output: Average time 62 ms
Input:30 ms - Output: Average time 63 ms
Input:50 ms - Output: Average time 93 ms
Input:100 ms - Output: Average time 141 ms
Input:250 ms - Output: Average time 281 ms
Input:500 ms - Output: Average time 532 ms
Input:1000 ms - Output: Average time 1031 ms

pathping 127.0.0.1 -n -q 1 -p 100 >nul

Here was a discussion about proper wait/delay in milliseconds.According to the last posts pathping should do the work for the milliseconds.

with mshta (this will wait 500 milliseconds):

start "" /wait /min /realtime mshta "javascript:setTimeout(function(){close();},500)"

Another way with selfcompiled .NET executable:

@if (@X)==(@Y) @end /* JScript comment
@echo off
setlocal
::del %~n0.exe /q /f
::
:: For precision better call this like
:: call waitMS 500
:: in order to skip compilation in case there's already built .exe
:: as without pointed extension first the .exe will be called due to the ordering in PATEXT variable
::
::
for /f "tokens=* delims=" %%v in ('dir /b /s /a:-d  /o:-n "%SystemRoot%\Microsoft.NET\Framework\*jsc.exe"') do (
   set "jsc=%%v"
)

if not exist "%~n0.exe" (
    "%jsc%" /nologo /w:0 /out:"%~n0.exe" "%~dpsfnx0"
)


%~n0.exe %*

endlocal & exit /b %errorlevel%


*/


import System;
import System.Threading;

var arguments:String[] = Environment.GetCommandLineArgs();
function printHelp(){
    Console.WriteLine(arguments[0]+" N");
    Console.WriteLine(" N - milliseconds to wait");
    Environment.Exit(0);    
}

if(arguments.length<2){
    printHelp();
}

try{
    var wait:Int32=Int32.Parse(arguments[1]);
    System.Threading.Thread.Sleep(wait);
}catch(err){
    Console.WriteLine('Invalid Number passed');
    Environment.Exit(1);
}

Below I have three different pure script solutions for providing sub-second delays within a batch script. All of these solutions have a minimum delay that varies from machine to machine. As long as the minimum delay is exceeded, they each are generally accurate to within 10 milliseconds.

I used Aacini's test harness to test the accuracy of all three methods so the results can be compared directly with his results.


Batch Macro

This pure batch solution is very simple in concept. I get the current time (with 1/100 second precision), add the delay, and wait for that new time within a tight loop.

I've packaged this logic in an advanced batch macro technique that is tricky to understand and develop, but easy to use. By using a macro, there is no delay introduced by CALL. See Current batch macro syntax and historical development of batch macros if you want to learn more about batch macro theory, and how to write your own.

This pure batch solution has the smallest minimum delay of the three. On the two machines I've tested, the minimum delay ranged from ~15 to ~30 msec. This macro only supports delays less than 24 hours.

The major drawback with this macro is it consumes CPU resources - pegging a single CPU machine with a single core to 100% usage throughout the delay.

@echo off
setlocal disableDelayedExpansion

::********  Define the @delay macro  ****************

:: define LF as a Line Feed (newline) character
set ^"LF=^

^" Above empty line is required - do not remove

:: define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

set @delay=for %%# in (1 2) do if %%#==2 (%\n%
  for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "t1=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, arg1/=10"%\n%
  cmd /v:on /c for /l %%. in (^^^) do @for /f "tokens=1-4 delims=:.," %%a in ("^!time: =0^!"^^^) do @set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100,tDiff=t2-t1"^^^>nul^^^&(if ^^^^!tDiff^^^^! lss 0 set /a tDiff+=8640000^^^>nul^^^)^^^&if ^^^^!tDiff^^^^! geq ^^^^!arg1^^^^! exit%\n%
  endlocal%\n%
) else setlocal enableDelayedExpansion^&set arg1=


::**********  Demonstrate usage  ********************
echo Delaying for 1.25 seconds ...
%@delay% 1250
echo done.
echo(


::***********  Testing accuracy  ********************
setlocal enableDelayedExpansion
echo Testing accuracy:
set runs=10
for %%t in (10 20 30 40 50 70 100 250 500 1000) do (

  (
  set t0=!time!
  for /l %%p in (1,1,%runs%) do %@delay% %%t
  set t1=!time!
  )

  for /f "tokens=1-8 delims=:.," %%a in ("!t0: =0!:!t1: =0!") do (
    set /a "a=(((1%%e-1%%a)*60)+1%%f-1%%b)*6000+1%%g%%h-1%%c%%d, a+=(a>>31) & 8640000"
  )

  set /a average_time=a*10/runs
  echo(Input:%%t ms - Output: Average time !average_time! ms
)

-- Sample results --

Delaying for 1.25 seconds ...
done.

Testing accuracy:
Input:10 ms - Output: Average time 14 ms
Input:20 ms - Output: Average time 20 ms
Input:30 ms - Output: Average time 30 ms
Input:40 ms - Output: Average time 40 ms
Input:50 ms - Output: Average time 50 ms
Input:70 ms - Output: Average time 70 ms
Input:100 ms - Output: Average time 100 ms
Input:250 ms - Output: Average time 250 ms
Input:500 ms - Output: Average time 500 ms
Input:1000 ms - Output: Average time 1000 ms


SLEEP.BAT - Hybrid JScript/Batch (Locale Agnostic)

This is my favorite solution. I CALL my SLEEP.BAT utility, passing in the desired delay, plus the current %time% at the time of the CALL. The batch script calls JScript embedded within the same file, which subtracts the CALL time from the current time to determine how much time has already elapsed, and then subtracts this value from the delay time to figure out how long the script should sleep. This solution uses the WScript.sleep() method, which is event driven and does not consume CPU resources.

The batch script will supply its own %time% value if it is not passed in, but then the accuracy may suffer a bit due to the length of time it takes to CALL the utility.

It takes a significant time for CSCRIPT to initialize, so the minimum delay is longer than the macro solution. On my two machines, the minimum delay ranged from ~30 to ~55 msec.

SLEEP.BAT should work on any Windows machine, regardless how your locale displays date and time. The only problem I have with this utility is it can give the wrong delay if called the instant before changeover from standard to daylight savings time, or vice-versa.

SLEEP.BAT

@if (@X)==(@Y) @end /* harmless hybrid line that begins a JScript comment
@goto :batch
:::
:::SLEEP.BAT  msec  [%time%]
:::SLEEP.BAT  /?
:::
:::  Suspend processing for msec milliseconds. The optional %time% argument
:::  can be added to improve timing accuracy. If called within a FOR loop,
:::  then !time! should be used instead, after enabling delayed expansion.
:::
:::  There is a startup time for SLEEP.BAT that limits the shortest delay
:::  possible. The startup time varies from machine to machine. Delays longer
:::  than the minimum are usually accurate to within 10 msec if the %time%
:::  argument is provided. One exception is when SLEEP.BAT is called the
:::  instant before changing from standard to daylight savings time, in which
:::  case the delay is extended by the startup time. The other exception occurs
:::  when changing from daylight savings to standard, in which case the delay
:::  never exceeds the startup time.
:::
:::  A single /? argument displays this help.
:::
:::  SLEEP.BAT Version 1.0 - written by Dave Benham
:::

============= :Batch portion ===========
@echo off
if "%~1" equ "/?" (
  for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"') do @echo(%%A
  exit /b 0
) else cscript //E:JScript //nologo "%~f0" %* %time%
exit /b

============ JScript portion ==========*/
try {
  var arg2 = WScript.Arguments.Item(1).split(/[:.,]/);
  var start = new Date();
  if (start.getHours()<Number(arg2[0])) start.setDate( start.getDate()-1 );
  start.setHours( Number(arg2[0]),
                  Number(arg2[1]),
                  Number(arg2[2]),
                  Number(arg2[3])*10
                );
  var delay = Number(WScript.Arguments.Item(0));
  var adjustedDelay = delay - ((new Date())-start);
  if (adjustedDelay>0) WScript.sleep( (adjustedDelay>delay) ? delay : adjustedDelay );
  WScript.Quit(0);
} catch(e) {
  WScript.Stderr.WriteLine("SLEEP.BAT - Invalid call");
  WScript.Quit(1);
}

Test harness and demonstration of usage

@echo off
setlocal enableDelayedExpansion
set runs=10
for %%t in (20 30 40 50 70 100 250 500 1000) do (

  (
  set t0=!time!
  for /l %%p in (1,1,%runs%) do call sleep %%t !time!
  set t1=!time!
  )

  for /f "tokens=1-8 delims=:.," %%a in ("!t0: =0!:!t1: =0!") do (
    set /a "a=(((1%%e-1%%a)*60)+1%%f-1%%b)*6000+1%%g%%h-1%%c%%d, a+=(a>>31) & 8640000"
  )

  set /a average_time=a*10/runs
  echo(Input:%%t ms - Output: Average time !average_time! ms
)

-- Sample Output --

Input:20 ms - Output: Average time 56 ms
Input:30 ms - Output: Average time 55 ms
Input:40 ms - Output: Average time 54 ms
Input:50 ms - Output: Average time 56 ms
Input:70 ms - Output: Average time 71 ms
Input:100 ms - Output: Average time 100 ms
Input:250 ms - Output: Average time 253 ms
Input:500 ms - Output: Average time 501 ms
Input:1000 ms - Output: Average time 1001 ms


LocaleSleep.bat - Hybrid JScript/Batch (Locale Dependent)

This is nearly identical to SLEEP.BAT, except it passes in "%date% %time%" instead of "%time%. The advantage is it no longer has problems with the changeover between standard and daylight savings time. The major disadvantage is it is locale dependent. It only works if %date% is parsed properly by the Date object parse() method. I know this should work on most U.S. machines, plus some others. Variants could be written to support other locales, but each would be locale dependent.

LocaleSleep.bat

@if (@X)==(@Y) @end /* harmless hybrid line that begins a JScript comment
@goto :batch
:::
:::LocaleSleep.bat  msec  ["%date% %time%"]
:::LocaleSleep.bat  /?
:::
:::  Suspend processing for msec milliseconds. The optional "%date% %time%"
:::  argument can be added to improve timing accuracy. If called within a
:::  FOR loop, then "!date! !time!" should be used instead, after enabling
:::  delayed expansion.
:::
:::  This utility is locale specific. It only works properly if %date% is in
:::  a format that is parsed properly by the Date object parse() method.
:::
:::  There is a startup time for SLEEP.BAT that limits the shortest delay
:::  possible. The startup time varies from machine to machine. Delays longer
:::  than the minimum are usually accurate to within 10 msec if the
:::  "%date% %time%" argument is provided.
:::
:::  A single /? argument displays this help.
:::
:::  LocaleSleep.bat Version 1.0 - written by Dave Benham
:::

============= :Batch portion ===========
@echo off
if "%~1" equ "/?" (
  for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"') do @echo(%%A
  exit /b 0
) else cscript //E:JScript //nologo "%~f0" %* "%date% %time%"
exit /b

============ JScript portion ==========*/
try {
  var arg2 = WScript.Arguments.Item(1);
  var delay = Number(WScript.Arguments.Item(0)) - ((new Date())-(new Date(arg2.slice(0,-3))).setMilliseconds( Number(arg2.slice(-2))*10 ));
  if (delay>0) WScript.sleep(delay);
  WScript.Quit(0);
} catch(e) {
  WScript.Stderr.WriteLine("localeSleep.bat - Invalid call");
  WScript.Quit(1);
}

Test harness and demonstration of usage

@echo off
setlocal enableDelayedExpansion
set runs=10
for %%t in (20 30 40 50 70 100 250 500 1000) do (

  (
  set t0=!time!
  for /l %%p in (1,1,%runs%) do call localeSleep %%t "!date! !time!"
  set t1=!time!
  )

  for /f "tokens=1-8 delims=:.," %%a in ("!t0: =0!:!t1: =0!") do (
    set /a "a=(((1%%e-1%%a)*60)+1%%f-1%%b)*6000+1%%g%%h-1%%c%%d, a+=(a>>31) & 8640000"
  )

  set /a average_time=a*10/runs
  echo(Input:%%t ms - Output: Average time !average_time! ms
)

Results are virtually identical to SLEEP.BAT

Tags:

Batch File