22
33This module provides the infrastructure to measure Events Per Second (EPS)
44for different test cases, comparing FastPubSub performance against baseline.
5-
6- Usage:
7- python -m benchmarks.bench --case basic --duration 60
8- python -m benchmarks.bench --case raw_pubsub --duration 60
9- python -m benchmarks.bench --all --duration 60
105"""
116
127import argparse
2015from dataclasses import dataclass
2116from datetime import UTC , datetime
2217from pathlib import Path
23- from typing import Any , Protocol
18+ from typing import Any , Protocol , cast
2419
2520import psutil
2621
22+ from benchmarks .cases import BaselinePubSubTestCase , BasicTestCase
2723from fastpubsub .__about__ import __version__
2824
25+ BENCH_CASE_IMPLEMENTATIONS = {
26+ "all" : [BaselinePubSubTestCase , BasicTestCase ],
27+ "baseline" : [BaselinePubSubTestCase ],
28+ "basic" : [BasicTestCase ],
29+ }
30+
2931
3032class BenchmarkCase (Protocol ):
3133 """Protocol defining the interface for all benchmark test cases.
3234
3335 All benchmark cases must implement:
34- - EVENTS_PROCESSED: Counter for processed messages
36+ - num_msgs: The number of messages to be sent
3537 - case_name: Identifier for the test case
3638 - description: Human-readable description
37- - start(): Async context manager that runs the benchmark
39+ - all the methods included in this protocol
3840 """
3941
40- EVENTS_PROCESSED : int
42+ num_msgs : int
4143 case_name : str
4244 description : str
4345
@@ -49,6 +51,13 @@ def start(self) -> AbstractAsyncContextManager[float]:
4951 """
5052 ...
5153
54+ def get_total_processed_msgs (self ) -> int :
55+ """Get the sum of processed messages.
56+
57+ Returns:
58+ The total number of processed messages.
59+ """
60+
5261
5362@dataclass
5463class MeasureResult :
@@ -86,10 +95,10 @@ async def measure(case: BenchmarkCase, measure_time: int) -> AsyncGenerator[Meas
8695 """
8796 async with case .start () as start_time :
8897 while (elapsed_time := (time .time () - start_time )) < measure_time :
89- yield MeasureResult (case . EVENTS_PROCESSED , elapsed_time )
98+ yield MeasureResult (- 1 , elapsed_time )
9099 await asyncio .sleep (1.0 )
91100
92- yield MeasureResult (case .EVENTS_PROCESSED , time .time () - start_time )
101+ yield MeasureResult (case .get_total_processed_msgs () , time .time () - start_time )
93102
94103
95104async def run_benchmark (case : BenchmarkCase , measure_time : int ) -> MeasureResult :
@@ -107,13 +116,20 @@ async def run_benchmark(case: BenchmarkCase, measure_time: int) -> MeasureResult
107116 async for result in measure (case , measure_time ):
108117 # Clear line and print progress
109118 sys .stdout .write (
110- f"\r [{ case .case_name } ] Events: { result .total_events :,} , "
111- f"Time: { result .elapsed_time :.1f} s ({ (measure_time - result .elapsed_time ):.1f} s left), "
112- f"EPS: { result .eps :,.2f} "
119+ f"\r [{ case .case_name } ] Time: { result .elapsed_time :.1f} s "
120+ f"({ (measure_time - result .elapsed_time ):.1f} s left)"
113121 )
114122 sys .stdout .flush ()
115123
116- # Print newline after progress
124+ # Prints the final result and the final line
125+ sys .stdout .write (
126+ f"\r [{ case .case_name } ] Events: { result .total_events :,} , "
127+ f"Time: { result .elapsed_time :.1f} s "
128+ "({(measure_time - result.elapsed_time):.1f}s left), "
129+ f"EPS: { result .eps :,.2f} "
130+ )
131+ sys .stdout .flush ()
132+
117133 print ()
118134 return result
119135
@@ -149,6 +165,7 @@ def save_results(
149165 "Python Version" ,
150166 "Description" ,
151167 "Host Memory" ,
168+ "Number of Initial Messages" ,
152169 ]
153170 )
154171
@@ -163,6 +180,7 @@ def save_results(
163180 platform .python_version (),
164181 case .description ,
165182 f"{ mem .total / (1024 ** 3 ):.2f} GB" ,
183+ case .num_msgs ,
166184 ]
167185 )
168186
@@ -187,15 +205,18 @@ def print_results(cases_results: list[tuple[BenchmarkCase, MeasureResult]]) -> N
187205
188206 print ("-" * 60 )
189207
190- # Calculate overhead if we have both cases
191- if len ( cases_results ) == 2 :
192- raw_result = next (( r for c , r in cases_results if c . case_name == "raw_pubsub" ), None )
193- basic_result = next (( r for c , r in cases_results if c . case_name == "basic" ), None )
208+ # Calculate overhead for each test case against baseline.
209+ baseline_result = next (( r for c , r in cases_results if c . case_name == "baseline" ), None )
210+ if not baseline_result or baseline_result . eps <= 0 :
211+ return
194212
195- if raw_result and basic_result and raw_result .eps > 0 :
196- overhead = ((raw_result .eps - basic_result .eps ) / raw_result .eps ) * 100
197- print (f"FastPubSub overhead: { overhead :.1f} %" )
198- print ("=" * 60 )
213+ for c , r in cases_results :
214+ if c .case_name == "baseline" :
215+ continue
216+
217+ overhead = ((baseline_result .eps - r .eps ) / baseline_result .eps ) * 100
218+ print (f"FastPubSub ({ c .case_name } ) overhead: { overhead :.1f} %" )
219+ print ("=" * 60 )
199220
200221
201222async def main () -> None :
@@ -205,46 +226,42 @@ async def main() -> None:
205226 formatter_class = argparse .RawDescriptionHelpFormatter ,
206227 epilog = """
207228Examples:
208- python -m benchmarks.bench --case basic --duration 60
209- python -m benchmarks.bench --case raw_pubsub --duration 60
210- python -m benchmarks.bench --all --duration 60
229+ python -m benchmarks.bench --case basic --duration 60 --num-msgs 10
230+ python -m benchmarks.bench --case baseline --duration 60 --num-msgs 10
231+ python -m benchmarks.bench --case all --duration 60 --num-msgs 10
211232 """ ,
212233 )
213234 parser .add_argument (
214235 "--case" ,
215- choices = ["basic" , "raw_pubsub" ],
216- default = "basic" ,
217- help = "Benchmark case to run (default: basic)" ,
236+ choices = list (BENCH_CASE_IMPLEMENTATIONS .keys ()),
237+ default = "all" ,
238+ help = "Benchmark case to run (default: all)" ,
239+ )
240+ parser .add_argument (
241+ "--num-msgs" ,
242+ default = 100 ,
243+ help = "The number of messages to process (default: 100)" ,
218244 )
219245 parser .add_argument (
220246 "--duration" ,
221247 type = int ,
222248 default = 60 ,
223249 help = "Duration in seconds (default: 60)" ,
224250 )
225- parser .add_argument (
226- "--all" ,
227- action = "store_true" ,
228- help = "Run all benchmark cases" ,
229- )
230251
231252 args = parser .parse_args ()
232253
233- # Import cases here to avoid circular imports
234- from benchmarks .cases .basic import BasicTestCase
235- from benchmarks .cases .raw_pubsub import RawPubSubTestCase
254+ cases_cls : list [Any ] = cast (list [Any ], BENCH_CASE_IMPLEMENTATIONS .get (args .case , []))
255+ if not cases_cls :
256+ raise ValueError (f"No benchmark found for --case { args .case } " )
257+
258+ cases = []
259+ for case_cls in cases_cls :
260+ cases .append (case_cls (int (args .num_msgs )))
236261
237262 results_file = Path (__file__ ).resolve ().parent / "results" / "benches.csv"
238263 cases_results : list [tuple [Any , MeasureResult ]] = []
239264
240- cases : list [Any ]
241- if args .all :
242- cases = [RawPubSubTestCase (), BasicTestCase ()]
243- elif args .case == "basic" :
244- cases = [BasicTestCase ()]
245- else :
246- cases = [RawPubSubTestCase ()]
247-
248265 print (f"\n FastPubSub Benchmark Suite v{ __version__ } " )
249266 print (f"Duration: { args .duration } s per case" )
250267 print (f"Python: { platform .python_version ()} " )
0 commit comments