timer.f90 Source File


Source Code

!> @file timer.f90
!> @brief Wall-clock timer module for performance profiling.
!!
!! Provides a lightweight @p timer_t derived type built on
!! @p system_clock().  Wall-clock time is used (rather than @p cpu_time)
!! so that the timers remain meaningful for future MPI-parallel runs where
!! per-process wall time is the quantity of interest.
!!
!! Typical use:
!! @code
!!   type(timer_t) :: t
!!   call timer_start(t)
!!   call expensive_work()
!!   call timer_stop(t)
!!   call timer_report(t, 'expensive_work')
!! @endcode

module timer
  use iso_fortran_env, only: int64, real64
  implicit none
  private
  public :: timer_t, timer_start, timer_stop, timer_elapsed_s, &
            timer_elapsed_running_s, timer_report, timer_reset

  !> Wall-clock timer accumulator.
  !!
  !! All fields default to zero / false so a freshly declared variable is
  !! ready to use without explicit initialisation.
  type, public :: timer_t
    !> @p system_clock count recorded at the most recent @p timer_start call.
    integer(int64) :: count_start = 0_int64
    !> Accumulated elapsed @p system_clock counts across all start/stop pairs.
    integer(int64) :: count_total = 0_int64
    !> Number of completed start/stop pairs.
    integer :: n_calls = 0
    !> @p .true. between a @p timer_start and the corresponding @p timer_stop.
    logical :: running = .false.
  end type timer_t

contains

  ! ---------------------------------------------------------------------------
  !> Start (or resume) the timer.
  !!
  !! Records the current @p system_clock count.  Calling @p timer_start on an
  !! already-running timer overwrites the start count without incrementing
  !! @p n_calls, so nested starts are not supported.
  !!
  !! @param[inout] t  Timer to start.
  ! ---------------------------------------------------------------------------
  subroutine timer_start(t)
    type(timer_t), intent(inout) :: t
    call system_clock(t % count_start)
    t % running = .true.
  end subroutine timer_start

  ! ---------------------------------------------------------------------------
  !> Stop the timer and accumulate elapsed counts.
  !!
  !! Handles @p system_clock counter wrap-around: if the current count is less
  !! than @p count_start (wrap occurred), the elapsed counts are computed using
  !! the counter maximum returned by @p system_clock.
  !!
  !! @param[inout] t  Timer to stop.
  ! ---------------------------------------------------------------------------
  subroutine timer_stop(t)
    type(timer_t), intent(inout) :: t
    integer(int64) :: count_now, count_rate, count_max

    call system_clock(count_now, count_rate, count_max)
    if (count_now >= t % count_start) then
      t % count_total = t % count_total + (count_now - t % count_start)
    else
      ! Counter wrapped around: count_max + 1 is the cycle length.
      t % count_total = t % count_total &
                        + (count_max - t % count_start + count_now + 1_int64)
    end if
    t % n_calls = t % n_calls + 1
    t % running = .false.
  end subroutine timer_stop

  ! ---------------------------------------------------------------------------
  !> Return accumulated wall time in seconds.
  !!
  !! Calls @p system_clock once to obtain the current clock rate.  Suitable
  !! for end-of-run reporting; do not call in tight loops.
  !!
  !! @param[in] t  Timer to query.
  !! @return       Accumulated wall time [s].
  ! ---------------------------------------------------------------------------
  real(real64) function timer_elapsed_s(t)
    type(timer_t), intent(in) :: t
    integer(int64) :: count_rate, dummy_count

    call system_clock(dummy_count, count_rate)
    if (count_rate > 0_int64) then
      timer_elapsed_s = real(t % count_total, real64) / real(count_rate, real64)
    else
      timer_elapsed_s = 0.0_real64
    end if
  end function timer_elapsed_s

  ! ---------------------------------------------------------------------------
  !> Return elapsed wall time in seconds, including the current running segment.
  !!
  !! Unlike @p timer_elapsed_s, this function is safe to call while the timer
  !! is still running: if @p t%running is @p .true., the in-progress segment
  !! (from the last @p timer_start to now) is included in the result.
  !! If the timer is stopped it is equivalent to @p timer_elapsed_s.
  !!
  !! @param[in] t  Timer to query.
  !! @return       Elapsed wall time [s].
  ! ---------------------------------------------------------------------------
  real(real64) function timer_elapsed_running_s(t)
    type(timer_t), intent(in) :: t
    integer(int64) :: count_now, count_rate, count_max, count_extra

    call system_clock(count_now, count_rate, count_max)
    if (count_rate <= 0_int64) then
      timer_elapsed_running_s = 0.0_real64
      return
    end if

    count_extra = 0_int64
    if (t % running) then
      if (count_now >= t % count_start) then
        count_extra = count_now - t % count_start
      else
        count_extra = count_max - t % count_start + count_now + 1_int64
      end if
    end if
    timer_elapsed_running_s = &
      real(t % count_total + count_extra, real64) / real(count_rate, real64)
  end function timer_elapsed_running_s

  ! ---------------------------------------------------------------------------
  !> Reset a timer to the zero state.
  !!
  !! @param[inout] t  Timer to reset.
  ! ---------------------------------------------------------------------------
  subroutine timer_reset(t)
    type(timer_t), intent(inout) :: t
    t % count_start = 0_int64
    t % count_total = 0_int64
    t % n_calls = 0
    t % running = .false.
  end subroutine timer_reset

  ! ---------------------------------------------------------------------------
  !> Print a one-line performance report to stdout.
  !!
  !! Output format (80 columns):
  !! @code
  !!   <label padded to 20>  <elapsed s>   <n_calls> calls   <ms/call>
  !! @endcode
  !!
  !! @param[in] t      Timer to report.
  !! @param[in] label  Descriptive region name.
  ! ---------------------------------------------------------------------------
  subroutine timer_report(t, label)
    type(timer_t), intent(in) :: t
    character(len=*), intent(in) :: label

    real(real64) :: elapsed, ms_per_call

    elapsed = timer_elapsed_s(t)
    if (t % n_calls > 0) then
      ms_per_call = elapsed * 1.0e3_real64 / real(t % n_calls, real64)
    else
      ms_per_call = 0.0_real64
    end if
    write (*, '(2X,A20,A,ES9.3,A,I9,A,A,F10.4,A)') &
      label, '  ', elapsed, ' s', t % n_calls, ' calls', '  ', ms_per_call, ' ms/call'
  end subroutine timer_report

end module timer