77
88import 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+
1018if 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