diff options
| author | Павел Жуков <33721692+LeaveMyYard@users.noreply.github.com> | 2023-05-11 16:30:42 +0300 |
|---|---|---|
| committer | Павел Жуков <33721692+LeaveMyYard@users.noreply.github.com> | 2023-05-11 16:30:42 +0300 |
| commit | 717baa6b12e09b5103c05737c11b2f56dcf651ba (patch) | |
| tree | 0f53cb9f52ecc2d04c2bd8eaa44572767631a8ad /robusta_krr | |
| parent | 774731c008a3f648185270b0aa42ca13ac3ad1bf (diff) | |
Replace decimals on numpy arrays, keep timestapms
Diffstat (limited to 'robusta_krr')
| -rw-r--r-- | robusta_krr/core/abstract/strategies.py | 13 | ||||
| -rw-r--r-- | robusta_krr/core/integrations/prometheus/loader.py | 3 | ||||
| -rw-r--r-- | robusta_krr/core/integrations/prometheus/metrics/base_metric.py | 7 | ||||
| -rw-r--r-- | robusta_krr/core/models/allocations.py | 14 | ||||
| -rw-r--r-- | robusta_krr/core/runner.py | 24 | ||||
| -rw-r--r-- | robusta_krr/formatters/table.py | 9 | ||||
| -rw-r--r-- | robusta_krr/strategies/simple.py | 26 | ||||
| -rw-r--r-- | robusta_krr/utils/resource_units.py | 73 |
8 files changed, 88 insertions, 81 deletions
diff --git a/robusta_krr/core/abstract/strategies.py b/robusta_krr/core/abstract/strategies.py index 061f94a..750ce07 100644 --- a/robusta_krr/core/abstract/strategies.py +++ b/robusta_krr/core/abstract/strategies.py @@ -2,8 +2,9 @@ from __future__ import annotations import abc import datetime -from decimal import Decimal -from typing import Generic, Optional, TypeVar, get_args +import numpy as np +from numpy.typing import NDArray +from typing import Generic, Optional, TypeVar, get_args, Annotated, Literal import pydantic as pd @@ -12,8 +13,8 @@ from robusta_krr.utils.display_name import add_display_name class ResourceRecommendation(pd.BaseModel): - request: Optional[Decimal] - limit: Optional[Decimal] + request: Optional[float] + limit: Optional[float] class StrategySettings(pd.BaseModel): @@ -32,7 +33,9 @@ class StrategySettings(pd.BaseModel): _StrategySettings = TypeVar("_StrategySettings", bound=StrategySettings) -ResourceHistoryData = dict[str, list[Decimal]] + +ArrayNx2 = Annotated[NDArray[np.float64], Literal["N", 2]] +ResourceHistoryData = dict[str, ArrayNx2] HistoryData = dict[ResourceType, ResourceHistoryData] RunResult = dict[ResourceType, ResourceRecommendation] diff --git a/robusta_krr/core/integrations/prometheus/loader.py b/robusta_krr/core/integrations/prometheus/loader.py index 973fad7..74cc049 100644 --- a/robusta_krr/core/integrations/prometheus/loader.py +++ b/robusta_krr/core/integrations/prometheus/loader.py @@ -18,6 +18,9 @@ from robusta_krr.utils.service_discovery import ServiceDiscovery from .metrics import BaseMetricLoader +import numpy as np +from numpy.typing import NDArray + class PrometheusDiscovery(ServiceDiscovery): def find_prometheus_url(self, *, api_client: Optional[ApiClient] = None) -> Optional[str]: diff --git a/robusta_krr/core/integrations/prometheus/metrics/base_metric.py b/robusta_krr/core/integrations/prometheus/metrics/base_metric.py index 408715a..be180f6 100644 --- a/robusta_krr/core/integrations/prometheus/metrics/base_metric.py +++ b/robusta_krr/core/integrations/prometheus/metrics/base_metric.py @@ -3,7 +3,6 @@ from __future__ import annotations import abc import asyncio import datetime -from decimal import Decimal from typing import TYPE_CHECKING, Callable, TypeVar from robusta_krr.core.abstract.strategies import ResourceHistoryData @@ -11,6 +10,8 @@ from robusta_krr.core.models.config import Config from robusta_krr.core.models.objects import K8sObjectData from robusta_krr.utils.configurable import Configurable +import numpy as np + if TYPE_CHECKING: from ..loader import CustomPrometheusConnect @@ -54,11 +55,11 @@ class BaseMetricLoader(Configurable, abc.ABC): if result == []: self.warning(f"Prometheus returned no {self.__class__.__name__} metrics for {object}") - return {pod.name: [] for pod in object.pods} + return {pod.name: np.array([]) for pod in object.pods} pod_results = {pod: result[i] for i, pod in enumerate(object.pods)} return { - pod.name: [Decimal(value) for _, value in pod_result[0]["values"]] + pod.name: np.array([(timestamp, value) for timestamp, value in pod_result[0]["values"]], dtype=np.float64) for pod, pod_result in pod_results.items() if pod_result != [] } diff --git a/robusta_krr/core/models/allocations.py b/robusta_krr/core/models/allocations.py index dd7c94f..e3e0e14 100644 --- a/robusta_krr/core/models/allocations.py +++ b/robusta_krr/core/models/allocations.py @@ -1,7 +1,7 @@ from __future__ import annotations import enum -from decimal import Decimal +import math from typing import Literal, TypeVar, Union import pydantic as pd @@ -20,7 +20,7 @@ class ResourceType(str, enum.Enum): Memory = "memory" -RecommendationValue = Union[Decimal, Literal["?"], None] +RecommendationValue = Union[float, Literal["?"], None] Self = TypeVar("Self", bound="ResourceAllocations") @@ -30,21 +30,21 @@ class ResourceAllocations(pd.BaseModel): limits: dict[ResourceType, RecommendationValue] @staticmethod - def __parse_resource_value(value: Decimal | str | None) -> RecommendationValue: + def __parse_resource_value(value: float | str | None) -> RecommendationValue: if value is None: return None if isinstance(value, str): - return resource_units.parse(value) + return float(resource_units.parse(value)) - if value.is_nan(): + if math.isnan(value): return "?" - return value + return float(value) @pd.validator("requests", "limits", pre=True) def validate_requests( - cls, value: dict[ResourceType, Decimal | str | None] + cls, value: dict[ResourceType, float | str | None] ) -> dict[ResourceType, RecommendationValue]: return { resource_type: cls.__parse_resource_value(resource_value) for resource_type, resource_value in value.items() diff --git a/robusta_krr/core/runner.py b/robusta_krr/core/runner.py index f4d4d94..7246ae4 100644 --- a/robusta_krr/core/runner.py +++ b/robusta_krr/core/runner.py @@ -1,6 +1,5 @@ import asyncio import math -from decimal import Decimal from typing import Optional, Union from robusta_krr.core.abstract.strategies import ResourceRecommendation, RunResult @@ -46,32 +45,29 @@ class Runner(Configurable): self.echo("\n", no_prefix=True) self.print_result(formatted) - def __get_resource_minimal(self, resource: ResourceType) -> Decimal: + def __get_resource_minimal(self, resource: ResourceType) -> float: if resource == ResourceType.CPU: - return Decimal(1 / 1000) * self.config.cpu_min_value + return 1 / 1000 * self.config.cpu_min_value elif resource == ResourceType.Memory: - return Decimal(1_000_000) * self.config.memory_min_value + return 1_000_000 * self.config.memory_min_value else: - return Decimal(0) + return 0 - def _round_value(self, value: Optional[Decimal], resource: ResourceType) -> Optional[Decimal]: - if value is None: + def _round_value(self, value: Optional[float], resource: ResourceType) -> Optional[float]: + if value is None or math.isnan(value): return value - if value.is_nan(): - return Decimal("nan") - if resource == ResourceType.CPU: # NOTE: We use 10**3 as the minimal value for CPU is 1m - prec_power = Decimal(10**3) + prec_power = 10**3 elif resource == ResourceType.Memory: # NOTE: We use 10**6 as the minimal value for memory is 1M - prec_power = 1 / Decimal(10**6) + prec_power = 1 / 10**6 else: # NOTE: We use 1 as the minimal value for other resources - prec_power = Decimal(1) + prec_power = 1 - rounded = Decimal(math.ceil(value * prec_power)) / prec_power + rounded = math.ceil(value * prec_power) / prec_power minimal = self.__get_resource_minimal(resource) return max(rounded, minimal) diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py index 043e383..8a93f04 100644 --- a/robusta_krr/formatters/table.py +++ b/robusta_krr/formatters/table.py @@ -12,7 +12,6 @@ from robusta_krr.utils import resource_units NONE_LITERAL = "none" NAN_LITERAL = "?" -PRESCISION = 4 ALLOWED_DIFFERENCE = 0.05 @@ -21,13 +20,13 @@ class TableFormatter(BaseFormatter): __display_name__ = "table" - def _format_united_decimal(self, value: RecommendationValue, prescision: Optional[int] = None) -> str: + def _format(self, value: RecommendationValue) -> str: if value is None: return NONE_LITERAL elif isinstance(value, str): return NAN_LITERAL else: - return resource_units.format(value, prescision=prescision) + return resource_units.format(value) def _format_request_str(self, item: ResourceScan, resource: ResourceType, selector: str) -> str: allocated = getattr(item.object.allocations, selector)[resource] @@ -36,9 +35,9 @@ class TableFormatter(BaseFormatter): return ( f"[{severity.color}]" - + self._format_united_decimal(allocated) + + self._format(allocated) + " -> " - + self._format_united_decimal(recommended.value, prescision=PRESCISION) + + self._format(recommended.value) + f"[/{severity.color}]" ) diff --git a/robusta_krr/strategies/simple.py b/robusta_krr/strategies/simple.py index 5dba2db..456123c 100644 --- a/robusta_krr/strategies/simple.py +++ b/robusta_krr/strategies/simple.py @@ -1,6 +1,6 @@ -from decimal import Decimal - import pydantic as pd +import numpy as np +from numpy.typing import NDArray from robusta_krr.core.abstract.strategies import ( BaseStrategy, @@ -14,26 +14,26 @@ from robusta_krr.core.abstract.strategies import ( class SimpleStrategySettings(StrategySettings): - cpu_percentile: Decimal = pd.Field( + cpu_percentile: float = pd.Field( 99, gt=0, le=100, description="The percentile to use for the CPU recommendation." ) - memory_buffer_percentage: Decimal = pd.Field( + memory_buffer_percentage: float = pd.Field( 5, gt=0, description="The percentage of added buffer to the peak memory usage for memory recommendation." ) - def calculate_memory_proposal(self, data: dict[str, list[Decimal]]) -> Decimal: - data_ = [value for values in data.values() for value in values] + def calculate_memory_proposal(self, data: dict[str, NDArray[np.float64]]) -> float: + data_ = [np.max(values[:, 1]) for values in data.values()] if len(data_) == 0: - return Decimal("NaN") + return float("NaN") - return max(data_) * Decimal(1 + self.memory_buffer_percentage / 100) + return float(max(data_) * (1 + self.memory_buffer_percentage / 100)) - def calculate_cpu_proposal(self, data: dict[str, list[Decimal]]) -> Decimal: - data_ = [value for values in data.values() for value in values] - if len(data_) == 0: - return Decimal("NaN") + def calculate_cpu_proposal(self, data: dict[str, NDArray[np.float64]]) -> float: + if len(data) == 0: + return float("NaN") - return data_[int((len(data_) - 1) * self.cpu_percentile / 100)] + data_ = np.concatenate([values[:, 1] for values in data.values()]) if len(data) > 1 else list(data.values())[0] + return float(np.percentile(data_, self.cpu_percentile / 100)) class SimpleStrategy(BaseStrategy[SimpleStrategySettings]): diff --git a/robusta_krr/utils/resource_units.py b/robusta_krr/utils/resource_units.py index d80ac05..cf61df7 100644 --- a/robusta_krr/utils/resource_units.py +++ b/robusta_krr/utils/resource_units.py @@ -1,48 +1,53 @@ -from decimal import Decimal -from typing import Optional +from typing import Literal UNITS = { - "m": Decimal("1e-3"), - "Ki": Decimal(1024), - "Mi": Decimal(1024**2), - "Gi": Decimal(1024**3), - "Ti": Decimal(1024**4), - "Pi": Decimal(1024**5), - "Ei": Decimal(1024**6), - "k": Decimal(1e3), - "M": Decimal(1e6), - "G": Decimal(1e9), - "T": Decimal(1e12), - "P": Decimal(1e15), - "E": Decimal(1e18), + "m": 0.001, + "Ki": 1024, + "Mi": 1024**2, + "Gi": 1024**3, + "Ti": 1024**4, + "Pi": 1024**5, + "Ei": 1024**6, + "k": 1e3, + "M": 1e6, + "G": 1e9, + "T": 1e12, + "P": 1e15, + "E": 1e18, } -def parse(x: str) -> Decimal: +def parse(x: str, /) -> float | int: """Converts a string to an integer with respect of units.""" + for unit, multiplier in UNITS.items(): if x.endswith(unit): - return Decimal(x[: -len(unit)]) * multiplier - return Decimal(x) + return int(x[: -len(unit)]) * multiplier + if "." in x: + return float(x) + return int(x) -def format(x: Decimal, prescision: Optional[int] = None) -> str: - """Converts an integer to a string with respect of units.""" +def get_base(x: str, /) -> Literal[1024, 1000]: + """Returns the base of the unit.""" - if prescision is not None: - # Use inly the first prescision digits, starting from the biggest one - # Example? 123456 -> 123000 - assert prescision >= 0 + for unit, multiplier in UNITS.items(): + if x.endswith(unit): + return 1024 if unit in ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"] else 1000 + return 1000 if "." in x else 1024 + + +def format(x: float | int, /, *, base: Literal[1024, 1000] = 1024) -> str: + """Converts an integer to a string with respect of units.""" - exponent: int - sign, digits, exponent = x.as_tuple() # type: ignore - x = Decimal((sign, list(digits[:prescision]) + [0] * (len(digits) - prescision), exponent)) + if x < 1: + return f"{int(x*1000)}m" - if x == 0: - return "0" + units = ['', 'K', 'M', 'G', 'T', 'P', 'E'] + binary_units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei'] - for unit, multiplier in reversed(UNITS.items()): - if x % multiplier == 0: - v = int(x / multiplier) - return f"{v}{unit}" - return str(x) + x = int(x) + for i, unit in enumerate(binary_units if base == 1024 else units): + if x < base**(i + 1) or i == len(units) - 1 or x / base**(i + 1) < 10: + return f"{x/base**i:.0f}{unit}" + return f"{x/6**i:.0f}{unit}" |
