Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove units for machine readability #125

Merged
merged 23 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 41 additions & 19 deletions src/con_duct/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ class SystemInfo:
class ProcessStats:
pcpu: float # %CPU
pmem: float # %MEM
rss: int # Memory Resident Set Size in KiB
vsz: int # Virtual Memory size in KiB
rss: int # Memory Resident Set Size in Bytes
vsz: int # Virtual Memory size in Bytes
timestamp: str

def max(self, other: ProcessStats) -> ProcessStats:
Expand All @@ -96,6 +96,21 @@ def max(self, other: ProcessStats) -> ProcessStats:
timestamp=max(self.timestamp, other.timestamp),
)

def __post_init__(self) -> None:
self._validate()

def _validate(self) -> None:
if not isinstance(self.pcpu, (float, int)):
raise TypeError(f"Expected 'pcpu' to be of type 'float' or 'int', got {type(self.pcpu).__name__}")
if not isinstance(self.pmem, (float, int)):
raise TypeError(f"Expected 'pmem' to be of type 'float' or 'int', got {type(self.pmem).__name__}")
if not isinstance(self.rss, int):
raise TypeError(f"Expected 'rss' to be of type 'int', got {type(self.rss).__name__}")
if not isinstance(self.vsz, int):
raise TypeError(f"Expected 'vsz' to be of type 'int', got {type(self.vsz).__name__}")
if not isinstance(self.timestamp, str):
raise TypeError(f"Expected 'timestamp' to be of type 'str', got {type(self.timestamp).__name__}")


@dataclass
class LogPaths:
Expand Down Expand Up @@ -158,6 +173,7 @@ class Averages:
num_samples: int = 0

def update(self: Averages, other: Sample) -> None:
self._assert_num(other.total_rss, other.total_vsz, other.total_pmem, other.total_pcpu)
self.num_samples += 1
self.rss += (other.total_rss - self.rss) / self.num_samples
self.vsz += (other.total_vsz - self.vsz) / self.num_samples
Expand All @@ -166,6 +182,7 @@ def update(self: Averages, other: Sample) -> None:

@classmethod
def from_sample(cls, sample: Sample) -> Averages:
cls._assert_num(sample.total_rss, sample.total_vsz, sample.total_pmem, sample.total_pcpu)
return cls(
rss=sample.total_rss,
vsz=sample.total_vsz,
Expand All @@ -174,6 +191,11 @@ def from_sample(cls, sample: Sample) -> Averages:
num_samples=1,
)

@staticmethod
def _assert_num(*values: Any) -> None:
for value in values:
assert isinstance(value, (float, int))


@dataclass
class Sample:
Expand Down Expand Up @@ -217,8 +239,8 @@ def for_json(self) -> dict[str, Any]:
"totals": { # total of all processes during this sample
"pmem": self.total_pmem,
"pcpu": self.total_pcpu,
"rss_kb": self.total_rss,
"vsz_kb": self.total_vsz,
"rss": self.total_rss,
"vsz": self.total_vsz,
},
"averages": asdict(self.averages) if self.averages.num_samples >= 1 else {},
}
Expand Down Expand Up @@ -319,8 +341,8 @@ def collect_sample(self) -> Sample:
ProcessStats(
pcpu=float(pcpu),
pmem=float(pmem),
rss=int(rss),
vsz=int(vsz),
rss=int(rss) * 1024,
vsz=int(vsz) * 1024,
timestamp=datetime.now().astimezone().isoformat(),
),
)
Expand All @@ -345,46 +367,46 @@ def execution_summary(self) -> dict[str, Any]:
"exit_code": self.process.returncode,
"command": self.command,
"logs_prefix": self.log_paths.prefix,
"wall_clock_time": f"{self.elapsed_time:.3f} sec",
"wall_clock_time": f"{self.elapsed_time:.3f}",
"peak_rss": (
f"{self.max_values.total_rss} KiB"
f"{self.max_values.total_rss}"
if self.max_values.stats
else "unknown"
),
"average_rss": (
f"{self.averages.rss:.3f} KiB"
f"{int(self.averages.rss)}"
if self.averages.num_samples >= 1
else "unknown"
),
"peak_vsz": (
f"{self.max_values.total_vsz} KiB"
f"{self.max_values.total_vsz}"
if self.max_values.stats
else "unknown"
),
"average_vsz": (
f"{self.averages.vsz:.3f} KiB"
f"{int(self.averages.vsz)}"
if self.averages.num_samples >= 1
else "unknown"
),
"peak_pmem": (
f"{self.max_values.total_pmem}%"
f"{self.max_values.total_pmem}"
if self.max_values.stats
else "unknown%"
else "unknown"
),
"average_pmem": (
f"{self.averages.pmem:.3f}%"
f"{self.averages.pmem:.3f}"
if self.averages.num_samples >= 1
else "unknown%"
else "unknown"
),
"peak_pcpu": (
f"{self.max_values.total_pcpu}%"
f"{self.max_values.total_pcpu}"
if self.max_values.stats
else "unknown%"
else "unknown"
),
"average_pcpu": (
f"{self.averages.pcpu:.3f}%"
f"{self.averages.pcpu:.3f}"
if self.averages.num_samples >= 1
else "unknown%"
else "unknown"
),
"num_samples": self.averages.num_samples,
"num_reports": self.number,
Expand Down
89 changes: 89 additions & 0 deletions test/test_report.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations
from datetime import datetime
import pytest
from con_duct.__main__ import Averages, ProcessStats, Sample

stat0 = ProcessStats(
Expand Down Expand Up @@ -89,3 +91,90 @@ def test_averages_three_samples() -> None:
averages.update(sample2)
averages.update(sample2)
assert averages.pcpu == (stat0.pcpu + (2 * stat1.pcpu)) / 3


def test_process_stats_green() -> None:
# Assert does not raise
ProcessStats(
pcpu=1.0,
pmem=1.1,
rss=1024,
vsz=1025,
timestamp=datetime.now().astimezone().isoformat(),
)


def test_process_stats_handle_ints() -> None:
# Assert does not raise
ProcessStats(
pcpu=1,
pmem=1,
rss=1024,
vsz=1025,
timestamp=datetime.now().astimezone().isoformat(),
)


def test_process_stats_incorrect_pcpu_type() -> None:
with pytest.raises(TypeError):
ProcessStats(
pcpu="oopsie", # type: ignore[arg-type]
pmem=1.1,
rss=1024,
vsz=1025,
timestamp=datetime.now().astimezone().isoformat(),
)


def test_process_stats_incorrect_pmem_type() -> None:
with pytest.raises(TypeError):
ProcessStats(
pcpu=1.1,
pmem="oopsie", # type: ignore[arg-type]
rss=1024,
vsz=1025,
timestamp=datetime.now().astimezone().isoformat(),
)


def test_process_stats_incorrect_rss_type() -> None:
with pytest.raises(TypeError):
ProcessStats(
pcpu=1.1,
pmem=1.0,
rss="oopsie", # type: ignore[arg-type]
vsz=1025,
timestamp=datetime.now().astimezone().isoformat(),
)


def test_process_stats_incorrect_vsz_type() -> None:
with pytest.raises(TypeError):
ProcessStats(
pcpu=1.1,
pmem=1.0,
rss=1025,
vsz="oopsie", # type: ignore[arg-type]
timestamp=datetime.now().astimezone().isoformat(),
)


def test_process_stats_incorrect_ts_type() -> None:
with pytest.raises(TypeError):
ProcessStats(
pcpu=1.1,
pmem=1.0,
rss=1025,
vsz=1024,
timestamp=1 # type: ignore[arg-type]
)


def test_averages_assert_num_green() -> None:
# Assert does not raise
Averages._assert_num(0, 1, 1.0, 0.1)


def test_averages_assert_num_invalid() -> None:
with pytest.raises(AssertionError):
Averages._assert_num("oops") # type: ignore[arg-type]
Loading