Sunday 26 February 2012

Stopwatch in R

There was a discussion the other day on stackoverflow started by someone who wanted a stopwatch timer in R. It would start when they hit return, and stop when that line was finished executing. I guess they often have long-running jobs going and frequently think, "hmm, how long has that been running?".

I figured something could be done by using R's mechanisms for hooking code into various places, but there seems to be no way to hook in before code runs. There's the taskCallBack mechanism, but that happens at the end of the code, just before your prompt appears. I wasted a bit of time trying to figure something out using that, and gave up.

So the solution. A stopwatch function that evaluates its argument. You can do:

stopwatch(foo(a))
stopwatch({z=foo(a)})
stopwatch({x=foo(a);y=foo(b)})

and a handy little HH:MM:SS dialog will tick along until your code is finished. Then it will stop, and you can close it.

The dialog is done with the tcltk package, using a one-second timer tick. There's just the one main function, and a timeForm function that produces the HH:MM:SS display from a time in seconds:

stopwatch <- function(expr){
# tcltk R expression timer function, Barry Rowlingson Feb 2012
  require(tcltk)

  start=Sys.time()
  tt <- tktoplevel()

  tkwm.title(tt,gsub(" ","",paste(deparse(substitute(expr)),sep="",collapse=";")))

  closeme <- function(){
    tkdestroy(tt)
  }

  labelText <- tclVar("Running...")
  label1 <- tklabel(tt,text=tclvalue(labelText))
  tkconfigure(label1,textvariable=labelText)
  tkgrid(label1)
  e = environment()
  z <- function () {
    tclvalue(labelText)=paste("Running: ",timeForm(Sys.time()-start));
    assign("sw", tcl("after", 1000, z), env=e)
  }
  quit.but <- tkbutton(tt,text="Close",command=closeme)
  tkgrid(quit.but)
  sw = tcl("after", 1000, z)

  finished=function(){
    tcl("after","cancel",sw)
    tclvalue(labelText)=paste("Done after: ",timeForm(Sys.time()-start));
  }

  tryCatch(eval(expr),finally=finished())
}
 
timeForm <- function(t){
  t=as.numeric(t)
  s=t %% 60
  m=((t-s)/60) %% 60
  h = t %/% (60*60)
  stopifnot(s+m*60+h*60*60==t)
  tstring = sprintf("%02d",c(h,m,s))
  return(paste(tstring,collapse=":"))
  list(h=h,m=m,s=s)
}


This code is hereby released for anyone to do anything with, but keep the credit in the comment unless you've really changed a lot and its unrecognizable from my original...

I can think of a few improvements - possibly you might want to run a few things on different lines and have a cumulative stopwatch - a bit like split lap times.  You might then need a different paradigm, something like:

s = stopwatch()
timeOn(s,foo(a))
timeOn(s,foo(b))
foo(c)
timeOn(s,foo(c))

This would create a TclTk dialog with a timer that only runs within the timeOn evaluations.

Another possibility is a text-based timer rather than a GUI. You might also want it to return the time taken - currently it returns the value of the expression. Go ahead, knock yourself out.