Skip to content

Commit 4a6c82f

Browse files
committed
refactor of the analyzer
1 parent 8e0952b commit 4a6c82f

4 files changed

Lines changed: 182 additions & 131 deletions

File tree

src/app/config/constants.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,9 @@ class SamplePeriods(float, Enum):
216216
we have to extract a time series
217217
"""
218218

219-
STANDARD_TIME = 0.005 # 5 MILLISECONDS
219+
STANDARD_TIME = 0.01 # 10 MILLISECONDS
220220
MINIMUM_TIME = 0.001 # 1 MILLISECOND
221-
MAXIMUM_TIME = 0.1 # 10 MILLISECONDS
221+
MAXIMUM_TIME = 0.1 # 100 MILLISECONDS
222222

223223
# ======================================================================
224224
# CONSTANTS FOR EVENT METRICS
@@ -244,15 +244,31 @@ class AggregatedMetricName(StrEnum):
244244
"""aggregated metrics to calculate at the end of simulation"""
245245

246246
LATENCY_STATS = "latency_stats"
247-
THROUGHPUT_RPS = "throughput_rps"
247+
THROUGHPUT = "throughput_rps"
248248
LLM_STATS = "llm_stats"
249249

250250
# ======================================================================
251251
# CONSTANTS FOR SERVER RUNTIME
252252
# ======================================================================
253253

254254
class ServerResourceName(StrEnum):
255-
"""Keys for each server resource type, used when building the container map."""
255+
"""Keys for each server resource type, used when building the container map."""
256256

257-
CPU = "CPU"
258-
RAM = "RAM"
257+
CPU = "CPU"
258+
RAM = "RAM"
259+
260+
# ======================================================================
261+
# CONSTANTS FOR LATENCY STATS
262+
# ======================================================================
263+
264+
class LatencyKey(StrEnum):
265+
"""Keys for the collection of the latency stats"""
266+
267+
TOTAL_REQUESTS = "total_requests"
268+
MEAN = "mean"
269+
MEDIAN = "median"
270+
STD_DEV = "std_dev"
271+
P95 = "p95"
272+
P99 = "p99"
273+
MIN = "min"
274+
MAX = "max"

src/app/config/plot_constants.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Dataclass to define a central structure to plot the metrics"""
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass(frozen=True)
6+
class PlotCfg:
7+
"""Dataclass for the plot of the various metrics"""
8+
9+
no_data: str
10+
title: str
11+
x_label: str
12+
y_label: str
13+
ready_label: str | None = None
14+
io_label: str | None = None
15+
legend_label: str | None = None
16+
17+
LATENCY_PLOT = PlotCfg(
18+
no_data="No latency data",
19+
title="Request Latency Distribution",
20+
x_label="Latency (s)",
21+
y_label="Frequency",
22+
)
23+
24+
THROUGHPUT_PLOT = PlotCfg(
25+
no_data="No throughput data",
26+
title="Throughput (RPS)",
27+
x_label="Time (s)",
28+
y_label="Requests/s",
29+
)
30+
31+
32+
SERVER_QUEUES_PLOT = PlotCfg(
33+
no_data="No queue data",
34+
title="Server Queues",
35+
x_label="Time (s)",
36+
y_label="Queue length",
37+
ready_label="Ready queue",
38+
io_label="I/O queue",
39+
)
40+
41+
RAM_PLOT = PlotCfg(
42+
no_data="No RAM data",
43+
title="RAM Usage",
44+
x_label="Time (s)",
45+
y_label="RAM (MB)",
46+
legend_label="RAM",
47+
)

src/app/metrics/analyzer.py

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77

88
import numpy as np
99

10+
from app.config.constants import LatencyKey, SampledMetricName
11+
from app.config.plot_constants import (
12+
LATENCY_PLOT,
13+
RAM_PLOT,
14+
SERVER_QUEUES_PLOT,
15+
THROUGHPUT_PLOT,
16+
)
17+
1018
if TYPE_CHECKING:
1119

1220
from matplotlib.axes import Axes
@@ -26,6 +34,9 @@ class ResultsAnalyzer:
2634
- sampled metrics from servers and edges
2735
"""
2836

37+
# Class attribute to define the period to calculate the throughput in s
38+
_WINDOW_SIZE_S: float = 1
39+
2940
def __init__(
3041
self,
3142
*,
@@ -49,7 +60,7 @@ def __init__(
4960

5061
# Lazily computed caches
5162
self.latencies: list[float] | None = None
52-
self.latency_stats: dict[str, float] | None = None
63+
self.latency_stats: dict[LatencyKey, float] | None = None
5364
self.throughput_series: tuple[list[float], list[float]] | None = None
5465
self.sampled_metrics: dict[str, dict[str, list[float]]] | None = None
5566

@@ -72,36 +83,35 @@ def _process_event_metrics(self) -> None:
7283
if self.latencies:
7384
arr = np.array(self.latencies)
7485
self.latency_stats = {
75-
"total_requests": float(arr.size),
76-
"mean": float(np.mean(arr)),
77-
"median": float(np.median(arr)),
78-
"std_dev": float(np.std(arr)),
79-
"p95": float(np.percentile(arr, 95)),
80-
"p99": float(np.percentile(arr, 99)),
81-
"min": float(np.min(arr)),
82-
"max": float(np.max(arr)),
86+
LatencyKey.TOTAL_REQUESTS: float(arr.size),
87+
LatencyKey.MEAN: float(np.mean(arr)),
88+
LatencyKey.MEDIAN: float(np.median(arr)),
89+
LatencyKey.STD_DEV: float(np.std(arr)),
90+
LatencyKey.P95: float(np.percentile(arr, 95)),
91+
LatencyKey.P99: float(np.percentile(arr, 99)),
92+
LatencyKey.MIN: float(np.min(arr)),
93+
LatencyKey.MAX: float(np.max(arr)),
8394
}
8495
else:
8596
self.latency_stats = {}
8697

8798
# 3) Throughput per 1s window
8899
completion_times = sorted(clock.finish for clock in self._client.rqs_clock)
89-
window_size = 1.0
90100
end_time = self._settings.total_simulation_time
91101

92102
timestamps: list[float] = []
93103
rps_values: list[float] = []
94104
count = 0
95105
idx = 0
96-
current_end = window_size
106+
current_end = ResultsAnalyzer._WINDOW_SIZE_S
97107

98108
while current_end <= end_time:
99109
while idx < len(completion_times) and completion_times[idx] <= current_end:
100110
count += 1
101111
idx += 1
102112
timestamps.append(current_end)
103-
rps_values.append(count / window_size)
104-
current_end += window_size
113+
rps_values.append(count / ResultsAnalyzer._WINDOW_SIZE_S)
114+
current_end += ResultsAnalyzer._WINDOW_SIZE_S
105115
count = 0
106116

107117
self.throughput_series = (timestamps, rps_values)
@@ -122,7 +132,7 @@ def _extract_sampled_metrics(self) -> None:
122132

123133
self.sampled_metrics = metrics
124134

125-
def get_latency_stats(self) -> dict[str, float]:
135+
def get_latency_stats(self) -> dict[LatencyKey, float]:
126136
"""Return latency statistics, computing them if necessary."""
127137
self.process_all_metrics()
128138
return self.latency_stats or {}
@@ -139,73 +149,77 @@ def get_sampled_metrics(self) -> dict[str, dict[str, list[float]]]:
139149
assert self.sampled_metrics is not None
140150
return self.sampled_metrics
141151

142-
# TODO(Gioele Botta): create a class of constants to remove all magic words
143152
def plot_latency_distribution(self, ax: Axes) -> None:
144153
"""Draw a histogram of request latencies onto the given Axes."""
145154
if not self.latencies:
146-
ax.text(0.5, 0.5, "No latency data", ha="center", va="center")
155+
ax.text(0.5, 0.5, LATENCY_PLOT.no_data, ha="center", va="center")
147156
return
148157

149158
ax.hist(self.latencies, bins=50)
150-
ax.set_title("Request Latency Distribution")
151-
ax.set_xlabel("Latency (s)")
152-
ax.set_ylabel("Frequency")
159+
ax.set_title(LATENCY_PLOT.title)
160+
ax.set_xlabel(LATENCY_PLOT.x_label)
161+
ax.set_ylabel(LATENCY_PLOT.y_label)
153162
ax.grid(visible=True)
154163

155164
def plot_throughput(self, ax: Axes) -> None:
156165
"""Draw throughput (RPS) over time onto the given Axes."""
157166
timestamps, values = self.get_throughput_series()
158167
if not timestamps:
159-
ax.text(0.5, 0.5, "No throughput data", ha="center", va="center")
168+
ax.text(0.5, 0.5, THROUGHPUT_PLOT.no_data, ha="center", va="center")
160169
return
161170

162171
ax.plot(timestamps, values, marker="o", linestyle="-")
163-
ax.set_title("Throughput (RPS)")
164-
ax.set_xlabel("Time (s)")
165-
ax.set_ylabel("Requests/s")
172+
ax.set_title(THROUGHPUT_PLOT.title)
173+
ax.set_xlabel(THROUGHPUT_PLOT.x_label)
174+
ax.set_ylabel(THROUGHPUT_PLOT.y_label)
166175
ax.grid(visible=True)
167176

168177
def plot_server_queues(self, ax: Axes) -> None:
169178
"""Draw server queue lengths over time onto the given Axes."""
170179
metrics = self.get_sampled_metrics()
171-
ready = metrics.get("ready_queue_len", {})
172-
io_q = metrics.get("event_loop_io_sleep", {})
180+
ready = metrics.get(SampledMetricName.READY_QUEUE_LEN, {})
181+
io_q = metrics.get(SampledMetricName.EVENT_LOOP_IO_SLEEP, {})
173182

174183
if not (ready or io_q):
175-
ax.text(0.5, 0.5, "No queue data", ha="center", va="center")
184+
ax.text(0.5, 0.5, SERVER_QUEUES_PLOT.no_data, ha="center", va="center")
176185
return
177186

178187
samples = len(next(iter(ready.values()), []))
179188
times = np.arange(samples) * self._settings.sample_period_s
180189

181190
for sid, vals in ready.items():
182-
ax.plot(times, vals, label=f"{sid} (ready)")
191+
ax.plot(times, vals, label=f"{sid} {SERVER_QUEUES_PLOT.ready_label}")
183192
for sid, vals in io_q.items():
184-
ax.plot(times, vals, label=f"{sid} (I/O)", linestyle="--")
185-
186-
ax.set_title("Server Queues")
187-
ax.set_xlabel("Time (s)")
188-
ax.set_ylabel("Queue Length")
193+
ax.plot(
194+
times,
195+
vals,
196+
label=f"{sid} {SERVER_QUEUES_PLOT.io_label}",
197+
linestyle="--",
198+
)
199+
200+
ax.set_title(SERVER_QUEUES_PLOT.title)
201+
ax.set_xlabel(SERVER_QUEUES_PLOT.x_label)
202+
ax.set_ylabel(SERVER_QUEUES_PLOT.y_label)
189203
ax.legend()
190204
ax.grid(visible=True)
191205

192206
def plot_ram_usage(self, ax: Axes) -> None:
193207
"""Draw RAM usage over time onto the given Axes."""
194208
metrics = self.get_sampled_metrics()
195-
ram = metrics.get("ram_in_use", {})
209+
ram = metrics.get(SampledMetricName.RAM_IN_USE, {})
196210

197211
if not ram:
198-
ax.text(0.5, 0.5, "No RAM data", ha="center", va="center")
212+
ax.text(0.5, 0.5, RAM_PLOT.no_data, ha="center", va="center")
199213
return
200214

201215
samples = len(next(iter(ram.values())))
202216
times = np.arange(samples) * self._settings.sample_period_s
203217

204218
for sid, vals in ram.items():
205-
ax.plot(times, vals, label=f"{sid} RAM")
219+
ax.plot(times, vals, label=f"{sid} {RAM_PLOT.legend_label}")
206220

207-
ax.set_title("RAM Usage")
208-
ax.set_xlabel("Time (s)")
209-
ax.set_ylabel("RAM (MB)")
221+
ax.set_title(RAM_PLOT.title)
222+
ax.set_xlabel(RAM_PLOT.x_label)
223+
ax.set_ylabel(RAM_PLOT.y_label)
210224
ax.legend()
211225
ax.grid(visible=True)

0 commit comments

Comments
 (0)