!> @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