Source code for underworld.timing

##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
##                                                                                   ##
##  This file forms part of the Underworld geophysics modelling application.         ##
##                                                                                   ##
##  For full license and copyright information, please refer to the LICENSE.md file  ##
##  located at the project root, or contact the authors.                             ##
##                                                                                   ##
##~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~#~##
'''
This module implements some high level timing operations for Underworld,
allowing users to determine how walltime is divided between different
Underworld API calls.  Note that this module *only* records timing
for Underworld API calls, and has no way of knowing how much time has
been spent elsewhere (such as `numpy`, `scipy` etc). The total runtime
is also recorded which gives users an indication of how much time
is spent outside Underworld.

Timing routines enabled by this module should introduce negligible
computational overhead.

Only the root process records timing information.

Note that to utilise timing routines, you must first set the
'UW_ENABLE_TIMING' environment variable, and this must be done
before you call `import underworld`.

Example
-------
>>> import os
>>> os.environ["UW_ENABLE_TIMING"] = "1"
>>> import underworld as uw
>>> uw.timing.start()
>>> someMesh = uw.mesh.FeMesh_Cartesian()
>>> with someMesh.deform_mesh():
...     someMesh.data[0] = [0.1,0.1]
>>> uw.timing.stop()
>>> # uw.timing.print_table()   # This will print the data.
>>>                      # Commented out as not doctest friendly.
>>> del os.environ["UW_ENABLE_TIMING"]  # remove to prevent timing for future doctests

'''

from collections import defaultdict as _dd
import time as _time
import inspect as _inspect
import underworld as _uw
import os as _os

timing = False

[docs]def start(): """ Call this function to start recording timing data. """ global timing global _maxdepth global _currentDepth _maxdepth = 1 _currentDepth = 0 if _uw.mpi.rank == 0: if "UW_ENABLE_TIMING" in _os.environ: timing = True else: import warnings warnings.warn("Timing unable to start. You must set the `UW_ENABLE_TIMING` environment variable before " "importing `underworld`. See `underworld.timing` module documentation for further info.") global _starttime _starttime = _time.time()
[docs]def stop(): """ Call this function to stop recording timing data. Note that this is automatically called when `print_table()` is called. """ global timing global _endtime if timing: _endtime = _time.time() timing = False
[docs]def reset(): """ Reset timing data. Note that this function calls `stop()`, and the user must call `start()` to resume recording timing data. """ stop() global _hit_count _hit_count = _dd(lambda: [0,0.])
# go ahead and reset reset()
[docs]def get_data(group_by="line_routine"): """ Returns dict with timing data. Parameters ---------- group_by: str Reported timing data is grouped according to the following options: "line" : Calling line of code. "routine" : Class routine. "line_routine": Line&routine form an individual timing group. """ if _uw.mpi.rank != 0: return # function to convert key into useful text def linefunc( key ): if key[1].startswith("<ipython-input-"): spltstr = key[1].split("-") no_cell = int(spltstr[2]) no_line = key[2] return "Cell: {:>3} Line:{:>3}".format(no_cell,no_line) else: return "{}:{:>5}".format(key[1].split('/')[-1],key[2]) if group_by == "line": keyfunc = linefunc elif group_by == "routine": keyfunc = lambda key : key[0] elif group_by == "line_routine": keyfunc = lambda key : "{} {}".format(linefunc(key),key[0]) else: raise ValueError("'group_by' parameter should specify 'line', 'routine' 'line_routine'" ) # regroup data regrouped_dict = _dd(lambda: [0,0.]) for key, value in _hit_count.items(): data = regrouped_dict[(keyfunc(key),key[3])] data[0] += value[0] data[1] += value[1] return regrouped_dict
def _incrementDepth(): """ Manually increment depth counter. This is sometimes needed to let this module know that we are inside the Underworld API. In particular, for StgCompoundComponents, we manually record construction time and we need this to ensure that we do not record all the sub-calls within the constructors. """ if timing: global _currentDepth _currentDepth += 1 def _decrementDepth(): """ Manually decrement depth counter. """ if timing: global _currentDepth _currentDepth -= 1
[docs]def log_result( time, name, foffset=1 ): """ Allows the user to manually add entries to data. Parameters ---------- time: float Time spent. name: str Name to record to dataset. Note that the current stack information is generated internally and recorded. foffset: int Frame offset. This is useful when you want to record this measurement against a call up the stack list. Defaults to 1. """ global _currentDepth if timing: if _currentDepth < _maxdepth: frame =_inspect.currentframe() count=0 while count < foffset: count += 1 frame = frame.f_back f_info = _inspect.getframeinfo(frame,0) data = _hit_count[(name,f_info[0], f_info[1],_currentDepth+1)] data[0]+=1 data[1]+=time
_timedroutines = set() def _routine_timer_decorator(routine, class_name=None): """ This decorator replaces any routine with the timed equivalent. """ import inspect if class_name: recname = class_name+"."+routine.__name__+"()" else: recname = routine.__name__ def timed(*args, **kwargs): global _currentDepth if timing: _currentDepth += 1 if _currentDepth <= _maxdepth: frame =_inspect.currentframe().f_back # get frame above this one f_info = _inspect.getframeinfo(frame,0) ts = _time.time() result = routine(*args, **kwargs) # print(ts) te = _time.time() data = _hit_count[(recname,f_info[0],f_info[1],_currentDepth)] data[0]+= 1 data[1]+= (te - ts) _currentDepth -= 1 return result _currentDepth -= 1 # if we get here, we're not timing, call routine / return result. return routine(*args, **kwargs) # the follow copies various attributes (signatures, docstrings etc) from the # wrapped function to the wrapper function import functools functools.update_wrapper(timed,routine) return timed def _class_timer_decorator(cls): """ This decorator walks the provided class decorating its methods with timed equivalents. """ for attr in _inspect.getmembers(cls, _inspect.isfunction): if issubclass(cls, _uw._stgermain.StgCompoundComponent): # metaclass captures constructor timing if attr[0] in ["__init__","__call__","__del__", "_setup", "__new__"]: continue if attr[0] in ["__del__",]: continue # don't wrap destructors if attr[1] in _timedroutines: continue # do not double wrap! # if attr[0] not in cls.__dict__: # continue # do not wrap attribs belonging to parents.. they will be wrapped when parent is processed. if attr[0] == "convert": continue # There's great great difficulty in determining static methods nested with classes with children. # As such, we treat this as a special case, unfortunately. # created timed routine timedroutine = _routine_timer_decorator(attr[1], cls.__name__) # static methods need to be setup accordingly try: if isinstance(cls.__dict__[attr[0]], staticmethod): timedroutine = staticmethod(timedroutine) except: pass setattr(cls, attr[0], timedroutine) _timedroutines.add(timedroutine) # add to set of timed routines to avoid doubling up return cls # keep a global list of done classes and modules _donemods = set() _doneclss = set() def _add_timing_to_mod(mod): """ This function walks the provided module, seeking out classes to replace via the _class_timer_decorator. """ if "UW_ENABLE_TIMING" not in _os.environ: return import inspect moddir = _os.path.dirname(inspect.getfile(mod)) lendir = len(moddir) # first gather info mods = [] for guy in dir(mod): obj = getattr(mod,guy) # only wrap these if not (inspect.ismodule(obj) or inspect.isclass(obj) or inspect.ismethod(obj)): continue # make sure obj is from current module. # do in try loops because some (builtins) wont work try: objpath = _os.path.dirname(inspect.getfile(obj)) except: continue if objpath[0:lendir] != moddir: continue if inspect.ismodule(obj): # add list of submodules if obj not in _donemods: _donemods.add(obj) mods.append(obj) elif inspect.isclass(obj): # replace with timed version if obj not in _doneclss: timed_obj = _class_timer_decorator(obj) setattr(mod,guy,timed_obj) _doneclss.add(obj) for mod in mods: # recurse into submodules _add_timing_to_mod(mod)