Source code for otx.api.utils.time_utils
"""This module implements time related utility functions."""
# Copyright (C) 2021-2022 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
#
import datetime
import functools
import time
from typing import Optional
[docs]
def now() -> datetime.datetime:
"""Return the current UTC creation_date and time up to a millisecond accuracy.
This function is preferable over the Python datetime.datetime.now() function
because it uses the same accuracy (milliseconds) as MongoDB rather than microsecond accuracy.
Returns:
Date and time up to a millisecond precision.
"""
date = datetime.datetime.now(datetime.timezone.utc)
return date.replace(microsecond=(date.microsecond // 1000) * 1000)
# Debug tools
[docs]
def timeit(func):
"""This function can be used as a decorator as @timeit.
It will print out how long the function took to execute.
Args:
func: The decorated function
Returns:
The wrapped function
"""
@functools.wraps(func)
def new_func(*args, **kwargs):
start_time = time.time()
res = func(*args, **kwargs)
elapsed_time = time.time() - start_time
print(f"function [{func.__name__}] finished in {float(elapsed_time * 1000)} ms")
return res
return new_func
[docs]
class TimeEstimator:
"""The time estimator.
Estimate the remaining time given the progress, and the progress changes. The estimator starts estimation at a
starting progress that is not necessarily 0. This choice is motivated by the fact that the first percent of
progress often takes a much longer time than the following percents.
Args:
smoothing_factor (float): Smoothing factor for the exponentially
weighted moving average. There's a great explanation at
https://www.wallstreetmojo.com/ewma/
inflation_factor (float): The factor by which the initial total time
estimation is inflated to ensure decreasing
update_window (float): Last update happened at progress1, next update
will happen at (progress1 + update window)
starting_progress (float): The progress at which the time_remaining
estimation starts time estimation
"""
# pylint: disable=too-many-instance-attributes
def __init__(
self,
smoothing_factor: float = 0.02,
inflation_factor: float = 1.1,
update_window: float = 1.0,
starting_progress: float = 1.0,
):
self.estimated_total_time: Optional[float] = None
self.estimated_end_time: Optional[float] = None
self.first_update_progress = starting_progress
self.first_update_time: Optional[float] = None
self.last_update_progress: Optional[float] = None
self.estimated_remaining_time: Optional[float] = None
self.starting_progress = starting_progress
self.smoothing_factor = smoothing_factor
self.inflation_factor = inflation_factor
self.update_window = update_window
[docs]
def time_remaining_from_progress(self, progress: float) -> float:
"""Updates the current progress, and returns the estimated remaining time in seconds (float).
Args:
progress (float): The new progress (floating point percentage, 0.0 - 100.0)
Returns:
The expected remaining time in seconds (float)
"""
estimation = -1.0
if progress is not None and progress > 0:
self.update(progress)
estimation = self.get_time_remaining()
return estimation
[docs]
def get_time_remaining(self):
"""If the new estimation is higher than the previous one by up to 2 seconds, return old estimation.
Returns:
Estimated remaining time in seconds (float)
"""
new_estimation = self.estimated_end_time - time.time() if self.estimated_end_time is not None else -1.0
if self.estimated_remaining_time is None or not 0.0 < new_estimation - self.estimated_remaining_time < 2.0:
self.estimated_remaining_time = new_estimation
return self.estimated_remaining_time
[docs]
def update(self, progress: float):
"""Update the estimator with a new progress (floating point percentage, between 0.0 - 100.0).
Args:
progress (float): Progress of the process
Returns:
None
"""
if progress >= self.first_update_progress and self.last_update_progress is None:
self.first_update_progress = progress
self.last_update_progress = progress
self.first_update_time = time.time()
if self.last_update_progress is not None and progress - self.last_update_progress >= self.update_window:
if self.first_update_time is None or self.first_update_progress is None:
raise AssertionError(
"first_update_time and first_update_progress both can not be None when calling "
"TimeEstimator.update()."
)
self.last_update_progress = progress
# normalized progress since starting point for estimation
normalized_progress = (progress - self.first_update_progress) / (100 - self.first_update_progress)
time_elapsed = time.time() - self.first_update_time
estimated_total = time_elapsed / normalized_progress
if self.estimated_total_time is None:
# inflating the initial estimation to ensure decreasing time remaining
self.estimated_total_time = estimated_total * self.inflation_factor
else:
self.estimated_total_time = (
self.smoothing_factor * self.estimated_total_time + (1 - self.smoothing_factor) * estimated_total
)
self.estimated_end_time = self.first_update_time + self.estimated_total_time