summaryrefslogtreecommitdiff
path: root/robusta_krr
diff options
context:
space:
mode:
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
commit717baa6b12e09b5103c05737c11b2f56dcf651ba (patch)
tree0f53cb9f52ecc2d04c2bd8eaa44572767631a8ad /robusta_krr
parent774731c008a3f648185270b0aa42ca13ac3ad1bf (diff)
Replace decimals on numpy arrays, keep timestapms
Diffstat (limited to 'robusta_krr')
-rw-r--r--robusta_krr/core/abstract/strategies.py13
-rw-r--r--robusta_krr/core/integrations/prometheus/loader.py3
-rw-r--r--robusta_krr/core/integrations/prometheus/metrics/base_metric.py7
-rw-r--r--robusta_krr/core/models/allocations.py14
-rw-r--r--robusta_krr/core/runner.py24
-rw-r--r--robusta_krr/formatters/table.py9
-rw-r--r--robusta_krr/strategies/simple.py26
-rw-r--r--robusta_krr/utils/resource_units.py73
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}"