diff --git a/doc/lsst.verify/index.rst b/doc/lsst.verify/index.rst index 59e52c2..dcc63cd 100644 --- a/doc/lsst.verify/index.rst +++ b/doc/lsst.verify/index.rst @@ -119,4 +119,8 @@ Python API reference :no-main-docstr: :no-inheritance-diagram: +.. automodapi:: lsst.verify.timer + :no-main-docstr: + :no-inheritance-diagram: + .. _SQUASH: https://squash.lsst.codes diff --git a/python/lsst/verify/__init__.py b/python/lsst/verify/__init__.py index 455a3a7..4acaa31 100644 --- a/python/lsst/verify/__init__.py +++ b/python/lsst/verify/__init__.py @@ -41,4 +41,5 @@ from .jobmetadata import * from .job import * from .output import * +from . import timer from . import yamlpersistance diff --git a/python/lsst/verify/jobmetadata.py b/python/lsst/verify/jobmetadata.py index 104f0ef..9f6b7a7 100644 --- a/python/lsst/verify/jobmetadata.py +++ b/python/lsst/verify/jobmetadata.py @@ -196,7 +196,7 @@ def values(self): """Iterate over metadata values. Returns - ------ + ------- items : `~collections.abc.ValuesView` An iterable over all the values. """ diff --git a/python/lsst/verify/timer.py b/python/lsst/verify/timer.py new file mode 100644 index 0000000..5f350f1 --- /dev/null +++ b/python/lsst/verify/timer.py @@ -0,0 +1,70 @@ +# This file is part of verify. +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__all__ = ["time_this_to_measurement"] + +from contextlib import contextmanager +import time + +import astropy.units as u + +from .datum import Datum +from .measurement import Measurement + + +def _epoch_to_iso(seconds): + """Convert a time in seconds since Unix epoch to an ISO 8601 timestamp. + + Parameters + ---------- + seconds : `float` + The number of seconds since the Unix epoch. + + Returns + ------- + timestamp : `str` + The input time represented as a timestamp. + """ + iso_format = "%Y-%m-%dT%H:%M:%SZ" + return time.strftime(iso_format, time.gmtime(seconds)) + + +@contextmanager +def time_this_to_measurement(measurement: Measurement): + """Time the enclosed block and record it as an lsst.verify measurement. + + Parameters + ---------- + measurement : `lsst.verify.Measurement` + Measurement object to fill with the timing information. Its metric must + have time dimensions. Any properties other than ``metric`` and + ``metric_name`` may be overwritten. + """ + start = time.time() + try: + yield + finally: + end = time.time() + measurement.quantity = (end - start) * u.second + # Same metadata as provided by TimingMetricTask + measurement.notes["estimator"] = "verify.timer.time_this_to_measurement" + measurement.extras["start"] = Datum(_epoch_to_iso(start)) + measurement.extras["end"] = Datum(_epoch_to_iso(end)) diff --git a/tests/test_timer.py b/tests/test_timer.py new file mode 100644 index 0000000..178d8af --- /dev/null +++ b/tests/test_timer.py @@ -0,0 +1,95 @@ +# This file is part of verify. +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import unittest + +import astropy.units as u + +import lsst.utils.tests + +from lsst.verify import Measurement, Metric, Name +from lsst.verify.timer import time_this_to_measurement + + +class TimeThisTestSuite(unittest.TestCase): + def setUp(self): + super().setUp() + self.metric_name = Name("verify.DummyTime") + + def test_basic(self): + duration = 0.2 + meas = Measurement(self.metric_name) + with time_this_to_measurement(meas): + time.sleep(duration) + + self.assertEqual(meas.metric_name, self.metric_name) # Should not have changed + self.assertIsNotNone(meas.quantity) + self.assertGreater(meas.quantity, duration * u.second) + self.assertLess(meas.quantity, 2 * duration * u.second) + + def test_unit_checking_ok(self): + duration = 0.2 + metric = Metric(self.metric_name, "Unconventional metric", u.nanosecond) + meas = Measurement(metric) + with time_this_to_measurement(meas): + time.sleep(duration) + + self.assertEqual(meas.metric_name, self.metric_name) # Should not have changed + self.assertIsNotNone(meas.quantity) + self.assertGreater(meas.quantity, duration * u.second) + self.assertLess(meas.quantity, 2 * duration * u.second) + + def test_unit_checking_bad(self): + duration = 0.2 + metric = Metric(self.metric_name, "Non-temporal metric", u.meter / u.second) + meas = Measurement(metric) + with self.assertRaises(TypeError): + with time_this_to_measurement(meas): + time.sleep(duration) + + def test_exception(self): + duration = 0.2 + meas = Measurement(self.metric_name) + try: + with time_this_to_measurement(meas): + time.sleep(duration) + raise RuntimeError("Something went wrong!") + except RuntimeError: + pass + + self.assertEqual(meas.metric_name, self.metric_name) # Should not have changed + self.assertIsNotNone(meas.quantity) + self.assertGreater(meas.quantity, duration * u.second) + self.assertLess(meas.quantity, 2 * duration * u.second) + + +class MemoryTester(lsst.utils.tests.MemoryTestCase): + pass + + +def setup_module(module): + lsst.utils.tests.init() + + +if __name__ == "__main__": + lsst.utils.tests.init() + unittest.main()