diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e33cff12d72291654f91077bcfa15fbc6d7947..9f5e83f652c2ecc1e317922c8c6c88282c3cb9f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,4 @@ * added kwargs to ThreadCollection.from_class * fixed a bug in CustomException +* added `parse_time_string` diff --git a/docs/time.rst b/docs/time.rst index 17613b9671635b431daf1e4f7722b766d9cbc4fa..e4c802c6576717e1c11acfe4f2b29e9155b5fa82 100644 --- a/docs/time.rst +++ b/docs/time.rst @@ -2,6 +2,14 @@ Time ==== +parse_time_string +----------------- + +Parse a time string into amount of seconds + +.. autofunction:: satella.time.parse_time_string + + measure ------- Sometimes you just need to measure how long does a routine call take. diff --git a/satella/__init__.py b/satella/__init__.py index badd328601d8b2d1a030cf8c0cfc9bd6963cab99..ffa161e81b0e7d22c1fdb82fa3d2a97e6e264ef9 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.14.46a3' +__version__ = '2.14.46' diff --git a/satella/instrumentation/cpu_time/collector.py b/satella/instrumentation/cpu_time/collector.py index 95e55f3208cc83483f913e8c135cf868a00fb05d..39f5ec805e026069b9f224d54892e081689ad620 100644 --- a/satella/instrumentation/cpu_time/collector.py +++ b/satella/instrumentation/cpu_time/collector.py @@ -5,8 +5,13 @@ import time import psutil +from satella.coding import for_argument from satella.coding.structures import Singleton from satella.coding.transforms import percentile +from satella.time import parse_time_string + +DEFAULT_REFRESH_EACH = '30m' +DEFAULT_WINDOW_SECONDS = '5m' @Singleton @@ -14,14 +19,18 @@ class CPUProfileBuilderThread(threading.Thread): """ A CPU profile builder thread and a core singleton object to use. - :param window_seconds: the amount of seconds for which to collect data - :param refresh_each: time of seconds to sleep between rebuilding of profiles + :param window_seconds: the amount of seconds for which to collect data. + Generally, this should be the interval during which your system cycles through all of + it's load, eg. if it asks it's devices each 5 minutes, the interval should be 300 seconds. + Or a time string. + :param refresh_each: time of seconds to sleep between rebuilding of profiles, or a time string. """ - def __init__(self, window_seconds: int = 300, refresh_each: int = 1800, + def __init__(self, window_seconds: tp.Union[str, int] = DEFAULT_WINDOW_SECONDS, + refresh_each: tp.Union[str, int] = DEFAULT_REFRESH_EACH, percentiles_requested: tp.Sequence[float] = (0.9, )): super().__init__(name='CPU profile builder', daemon=True) - self.window_size = window_seconds - self.refresh_each = refresh_each + self.window_size = parse_time_string(window_seconds) + self.refresh_each = parse_time_string(refresh_each) self.data = [] self.percentiles_requested = list(percentiles_requested) self.percentile_values = [] @@ -72,8 +81,7 @@ class CPUTimeManager: :param percent: float between 0 and 1 :return: the value of the percentile """ - cp = CPUProfileBuilderThread() - return cp.percentile(percent) + return CPUProfileBuilderThread().percentile(percent) @staticmethod def set_window_size(window_size: float) -> None: @@ -82,18 +90,18 @@ class CPUTimeManager: :param window_size: time, in seconds """ - cp = CPUProfileBuilderThread() - cp.window_size = window_size + CPUProfileBuilderThread().window_size = window_size -def sleep_cpu_aware(seconds: float, of_below: tp.Optional[float] = None, +@for_argument(parse_time_string) +def sleep_cpu_aware(seconds: tp.Union[str, float], of_below: tp.Optional[float] = None, of_above: tp.Optional[float] = None, check_each: float = 1) -> bool: """ Sleep for specified number of seconds. Quit earlier if the occupancy factor goes below of_below or above of_above - :param seconds: time to sleep + :param seconds: time to sleep in seconds, or a time string :param of_below: occupancy factor below which the sleep will return :param of_above: occupancy factor above which the sleep will return :param check_each: amount of seconds to sleep at once @@ -157,11 +165,13 @@ def _calculate_occupancy_factor() -> float: def calculate_occupancy_factor() -> float: """ - IMPORTANT! + Get the average load between now and the time it was last called as a float, + where 0.0 is LA=0 and 1.0 is LA=max_cores. This will be the average between now and the time it was last called. - This in rare cases (being called the first or the second time) may block for up to 0.1 seconds + .. warning:: This in rare cases (being called the first or the second time) may block for + up to 0.1 seconds :return: a float between 0 and 1 telling you how occupied CPU-wise is your system. """ diff --git a/satella/time.py b/satella/time.py index 6df47795b088892b8bf6866427a85b042f1c4f3e..7d7783639ed80eafbeaf1903f6c1126967ba8146 100644 --- a/satella/time.py +++ b/satella/time.py @@ -1,13 +1,13 @@ import copy import inspect -import math import time import typing as tp import warnings from concurrent.futures import Future from functools import wraps # import from functools to prevent circular import exception -__all__ = ['measure', 'time_as_int', 'time_ms', 'sleep', 'time_us', 'ExponentialBackoff'] +__all__ = ['measure', 'time_as_int', 'time_ms', 'sleep', 'time_us', 'ExponentialBackoff', + 'parse_time_string'] from satella.exceptions import WouldWaitMore @@ -364,3 +364,39 @@ class ExponentialBackoff: Called when something successes. """ self.counter = self.start + + +TIME_MODIFIERS = [ + ('s', 1), + ('m', 60), + ('h', 60*60), + ('d', 24*60*60), + ('w', 7*24*60*60) +] + + +def parse_time_string(s: tp.Union[int, float, str]) -> float: + """ + Parse a time string into seconds, so eg. '30m' will be equal to 1800, and so will + be '30 min'. + + This will correctly parse: + - seconds + - minutes + - hours + - days + - weeks + + .. warning:: This does not handle fractions of a second! + + :param s: time string or time value in seconds + :return: value in seconds + """ + if isinstance(s, (int, float)): + return s + + for modifier, multiple in TIME_MODIFIERS: + if modifier in s: + return float(s[:s.index(modifier)]) * multiple + + return float(s) diff --git a/tests/test_time.py b/tests/test_time.py index 0699f8f0b90905e1c06de1ffce3d27fb9c9012ff..1c323fdc2e7d9a7de2945b20beaf1835bf197da7 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -4,12 +4,20 @@ import time import multiprocessing import os import sys -from satella.time import measure, time_as_int, time_ms, sleep, ExponentialBackoff +from satella.time import measure, time_as_int, time_ms, sleep, ExponentialBackoff, \ + parse_time_string from concurrent.futures import Future class TestTime(unittest.TestCase): + def test_parse_time_string(self): + self.assertEqual(parse_time_string('30m'), 30 * 60) + self.assertEqual(parse_time_string('30h'), 30*60*60) + self.assertEqual(parse_time_string('30w'), 30 * 7 * 24 * 60 * 60) + self.assertEqual(parse_time_string(2), 2) + self.assertEqual(parse_time_string(2.0), 2.0) + def test_exponential_backoff(self): with measure() as measurement: eb = ExponentialBackoff()