The timer_tree_type module

The timer_tree_type module implements a lightweight method for creating and maintaining a nested tree of timers. The tree is automatically generated via the nested starting and stopping of named timers. Because timers are distinguished by name and position in the tree, a start/stop pair will give rise to different timers when the code containing it is executed within different timer nestings. As a result, the timing of shared code can be easily and automatically partitioned according to its use.

Basic usage

The most basic use of the timers requires no more than calling start_timer and stop_timer in matching pairs with an arbitrary name as in this example:

use timer_tree_type

call start_timer ('A')
  call start_timer ('B')
  call stop_timer  ('B')
  call start_timer ('C')
    call start_timer ('B')
    call stop_timer  ('B')
  call stop_timer ('C')
call stop_timer  ('A')
call start_timer ('B')
  call start_timer ('X')
  call stop_timer  ('X')
  call start_timer ('Y')
  call stop_timer  ('Y')
  call start_timer ('Z')
  call stop_timer  ('Z')
call stop_timer  ('B')
call start_timer ('A')
call stop_timer  ('A')

call write_timer_tree(unit=6, indent=2)

The only restriction is that the timers be nested: the timer most recently started (and still running) is the only one eligible to be stopped. Starting A, then B, and then stopping A is not allowed, for example. Any incorrect nesting of timers will be detected by stop_timer at run time. The calls, of course, can be in different program units.

The same name may be used for multiple start/stop pairs, and start/stop pairs with the same name and nesting are regarded as a single timer and their elapsed time accumulated accordingly. Otherwise they are regarded as distinct timers. An example of the latter is timer B. It is started and stopped at three different positions within the call nesting, and this is reflected in the associated timer tree pictured here:

digraph FLATLAND { 0 -> 1 -> 2; 1 -> 3 -> 4; 0 -> 5 -> 6; 5 -> 7; 5 -> 8; 0 [label="root"] 1 [label=A] 2 [label=B] 3 [label=C] 4 [label=B] 5 [label=B] 6 [label=X] 7 [label=Y] 8 [label=Z] }

Each pair is regarded as a distinct timer. Timer A, on the other hand, is an example of the former case. It is started and stopped twice, but at the same position within the call nesting, and thus appears in the timer tree only once. The elapsed time for each call pair is accumulated in the single timer as expected.

Finally, a call to write_timer_tree (at any time) will write the time accumulated thus far for each timer, using indentation to express the nested structure of the timer tree. The output would look something like this for the code example:

A: 4.90000E-02
  B: 1.00000E-02
  C: 1.90000E-02
    B: 9.00000E-03
B: 2.80000E-02
  X: 9.00000E-03
  Y: 1.00000E-02
  Z: 9.00000E-03

Because timers are distinguished by name and position in the tree, a start/stop pair will give rise to different timers when the code containing it is executed within different timer nestings. As a result, the timing of a portion of shared code can be easily and automatically partitioned according to its use. This is a distinctive feature of this implementation.

For portability the Fortran intrinsic subroutine system_clock is used to get the wall clock time, and the resolution of the timers is thus limited by the resolution of this subroutine, which can vary significantly between systems.

For most cases this covers everything one needs to know to in order to use the global timer tree managed by the timer_tree_type module. The next section describes some additional functionality that may be useful, and in section that follows it, the timer_tree derived type that underlies the implementation is described.

The global timer tree

The following subroutines operate on the global timer tree.

call start_timer(name [,handle])

Start the timer with the specified character string name that is a child of the current timer. If no such child exists, one is created with this name. This child timer then becomes the current timer. If the optional integer argument handle is specified, it returns a handle to the timer which can be used as an argument to write_timer_tree or read_timer.

call stop_timer(name [,stat [,errmsg]])

Stop the current timer. The current timer’s parent becomes the new current timer. It is an error if the current timer does not have the specified name. If the optional integer argument stat is present, it is assigned the value 0 if no error was encountered; otherwise it is assigned a non-zero value. In the latter case, the allocatable deferred-length character string errmsg, if present, is assigned an explanatory message. If stat is not present and an error occurs, the error message is written to the preconnected error unit and the program exits with a nonzero status.

call write_timer_tree(unit, indent [,handle])

Write the accumulated time for each timer to the specified logical unit, using indentation to express the nested structure of the timer tree. The incremental number of spaces to indent for successive tree levels is given by indent. If an optional integer handle returned by start_timer is specified, only the accumulated times for that timer and its decendents are written.

call read_timer(handle, time)

Returns in the default real argument time, the elapsed time for the timer corresponding to the handle returned by start_timer. The timer may be running or stopped.

call reset_timer_tree

resets the timer tree to its initial, empty state.

Accessing the timer tree data

Sometimes more direct access to the timer tree data is needed than is provided by either read_timer or write_timer_tree. For example, the data may need to be communicated between processes in a parallel simulation, or it may need to be written in a format that can be easily read and used to initialize the timer tree. The following subroutines provide such functionality.

call serialize_timer_tree(tree, name, time)

Get the current state of the timer tree in flat arrays. Timers may be running or stopped and their state is unaltered. The deferred-length, allocatable character array name and allocatable default real array time return the timer names and elapsed times indexed by tree node number. The allocatable default integer array tree returns the structure of the tree as a sequence of node numbers: node numbers appear in matching pairs, like opening and closing parentheses, with the intervening sequence describing the trees of its children, recusively. The nodes are numbered so that the initial node of the pairs appear in sequential order. This enables a simple reconstruction of the tree. All three allocatable array arguments are allocated by the subroutine; they are reallocated if necessary.

call deserialize_timer_tree(tree, name, time)

Define the state of the timer tree using the tree, name, and time arrays returned by serialize_timer_tree. This can be used to initialize the timer tree with results from a previous simulation, for example. Note that timer handles are not preserved.

Using the data from serialize_timer_tree

This code fragment shows how to use the output of serialize_timer_tree to reconstruct the tree by writing a group of nested XML tags that reproduce the structure of the tree. The integer m records the high-water mark of the tree node numbers encountered, which, due of the way the nodes are numbered, can be used to distinguish the initial and final node of each pair.

m = 0 ! the high-water mark of node indices encountered
do j = 1, size(tree)
  n = tree(j)
  if (n > m) then ! first encounter of this node index
    print *, '<TIMER NAME="', trim(name(n)), '" TIME="', time(n), '">'
    m = n
  else
    print *, '</TIMER>'
  end if
end do

The timer_tree derived type

The timer tree used thus far is in fact a type(timer_tree) object that is a private module variable in the timer_tree_type module (a ‘singleton’). The procedures described above all operate on this single global instance. But applications can also declare and use their own type(timer_tree) variables.

This derived type has the following type bound subroutines which have the same interface and effect as those described above, except that they operate on the specific instance rather than the global timer tree.

start(name [,handle])

Same interface as start_timer.

stop(name [,stat [,errmsg]])

Same interface as stop_timer.

write(unit, indent [,handle])

Same interface as write_timer_tree.

read(handle, time)

Same interface as read_timer.

serialize(tree, name, time)

Same interface as serialize_timer_tree.

deserialize(tree, name, time)

Same interface as deserialize_timer_tree.

Note that objects of this derived type:

  • are properly finalized when the object ceases to exist.

  • should not be used in assignment statements; only the default intrinsic assignment is available, and its semantics are unlikely to be what is wanted.