diff --git a/CHANGELOG.md b/CHANGELOG.md index baddb03..d672cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). ## Unreleased +## 0.10.2 - 2024-03-11 +### Added +- `time`: + - Keyword-only argument to `@duration` to report summary only every `interval`. + - A warning is generated when the argument is not used. + - `interval` can be set to `None` in order to disable automatic statistics. + - Argument `filename` that stores every measurement into the file. + +### Changed +- `time`: + - Timer summary now also contains minimum value. + ## 0.10.1 - 2024-03-07 ### Fixed - `duration`: diff --git a/README.md b/README.md index 17b078c..10fc78f 100644 --- a/README.md +++ b/README.md @@ -509,3 +509,9 @@ TM.end() TM.summary() return output ``` + +However, in contrast to TimeMeasurer, a keyword-only argument `interval` +can be passed to the decorator to report the summary periodically, and not +on every function call. + +Since `>0.10.1` not passing `interval` generates a warning on start-up. diff --git a/autopsy/time.py b/autopsy/time.py index aab9c06..b1b0bcb 100644 --- a/autopsy/time.py +++ b/autopsy/time.py @@ -50,6 +50,12 @@ def function(): TM.end() TM.summary() return output + +However, in contrast to TimeMeasurer, a keyword-only argument 'interval' +can be passed to the decorator to report the summary periodically, and not +on every function call. + +Since >0.10.1 not passing 'interval' generates a warning on start-up. """ ###################### # Imports & Globals @@ -58,6 +64,9 @@ def function(): # Py2: Allow import of module with the same name from __future__ import absolute_import +# ReportTimer +from threading import _Timer + # Timing import time @@ -87,6 +96,23 @@ def let_pass(f): return block if DISABLED else let_pass +###################### +# ReportTimer class +###################### + +class ReportTimer(_Timer): + """A thread for reporting the measurer. + + Source: + https://stackoverflow.com/questions/12435211/threading-timer-repeat-function-every-n-seconds + """ + + def run(self): + """Run a thread function every 'self.interval'.""" + while not self.finished.wait(self.interval): + self.function(*self.args, **self.kwargs) + + ###################### # Measurer class ###################### @@ -96,7 +122,7 @@ class Measurer(object): UNITS = ["", "s", "ms", "us", "ns"] - def __init__(self, name = "", unit = ""): + def __init__(self, name = "", unit = "", filename = "", **kwargs): """Initialize the Measurer class. Arguments @@ -105,11 +131,16 @@ def __init__(self, name = "", unit = ""): name of the Measurer unit: str = "" units used by the measurer + filename: str = "" + name of the file to log measured data Raises ------ ValueError when unit is not in UNITS + + IOError + raised by 'open()' """ if unit not in self.UNITS: raise ValueError("unknown unit '%s'" % unit) @@ -119,8 +150,10 @@ def __init__(self, name = "", unit = ""): self._start = 0 self._count = 0 self._sum = 0 + self._min = [] # min([], X) returns the number everytime self._max = 0 self._last = 0 + self._file = open(filename, "a") if filename != "" else None def unitExp(self, unit): @@ -191,8 +224,12 @@ def updateStatistics(self): """Update internal statistics of the Measurer.""" self._count += 1 self._sum += self._last + self._min = min(self._min, self._last) self._max = max(self._max, self._last) + if self._file is not None: + self._file.write("%f\n" % self._last) + @conddisable() def summary(self): @@ -200,8 +237,9 @@ def summary(self): if self._count == 0: print("%s: EMPTY" % self._name) else: - print("%s: cur=%.4f%s avg=%.4f%s max=%.4f%s" % ( + print("%s: cur=%.4f%s min=%.4f%s avg=%.4f%s max=%.4f%s" % ( self._name, self._last, self._unit, + self._min, self._unit, self._sum / self._count, self._unit, self._max, self._unit )) @@ -271,6 +309,19 @@ def duration(*args, **kwargs): TM = TimeMeasurer(*args, **kwargs) + if "interval" in kwargs: + if kwargs.get("interval") is not None: + report = ReportTimer(kwargs.get("interval"), TM.summary) + report.daemon = True + report.start() + elif "filename" in kwargs: + kwargs["interval"] = None + else: + print ( + "Warning: @duration is used without kwarg 'interval', " + "therefore it will report summary on every call." + ) + def wrapper(function, *args, **kwags): def duration_measurer(*args, **kwargs): TM.start() @@ -281,7 +332,18 @@ def duration_measurer(*args, **kwargs): return output return duration_measurer + def wrapper_with_timer(function, *args, **kwags): + def duration_measurer(*args, **kwargs): + TM.start() + output = function(*args, **kwargs) + TM.end() + + return output + return duration_measurer + def let_pass(function, *args, **kwargs): return lambda *x, **y: function(*x, **y) - return let_pass if DISABLED else wrapper + return let_pass if DISABLED else ( + wrapper if "interval" not in kwargs else wrapper_with_timer + ) diff --git a/package.xml b/package.xml index 13807ac..73af3b9 100644 --- a/package.xml +++ b/package.xml @@ -4,7 +4,7 @@ schematypens="http://www.w3.org/2001/XMLSchema"?> autopsy - 0.10.1 + 0.10.2 A set of Python utils for F1Tenth project. Jaroslav Klapálek