Performance simulation¶
Simulation script to demonstrate pipeline bottleneck scenarios.
This script creates a configurable multi-stage pipeline and runs with custom stage configurations to demonstrate how bottlenecks in different stages affect performance metrics.
See also
- Understanding the performance statistics
Uses this script to illustrates how pipeline configuration affects the performance statistics.
The script accepts flexible stage configurations via CLI arguments, where each stage can have: - Custom processing time (sleep duration in seconds) - Custom concurrency level (number of parallel tasks) - Optional aggregation (batch size for grouping items)
Performance statistics are collected at configurable intervals and saved to a SQLite database for analysis. The foreground thread simulates a consumer (e.g., training loop) with configurable sleep duration.
Example usage:
# Single stage with 50ms processing
python performance_simulation.py --db-path output.db --stage-configs "0.050,1"
# Three stages: fast (10ms) -> medium (25ms) -> slow (40ms)
python performance_simulation.py --db-path output.db --stage-configs "0.010,1;0.025,1;0.040,1"
# Stage with concurrency and aggregation
python performance_simulation.py --db-path output.db --stage-configs "0.006,1,1;0.0,1,4"
Source¶
Source
Click here to see the source.
1#!/usr/bin/env python3
2# Copyright (c) Meta Platforms, Inc. and affiliates.
3# All rights reserved.
4#
5# This source code is licensed under the BSD-style license found in the
6# LICENSE file in the root directory of this source tree.
7
8
9"""Simulation script to demonstrate pipeline bottleneck scenarios.
10
11This script creates a configurable multi-stage pipeline and runs with custom
12stage configurations to demonstrate how bottlenecks in different stages affect
13performance metrics.
14
15.. seealso::
16
17 :doc:`../optimization_guide/stats`
18 Uses this script to illustrates how pipeline configuration affects
19 the performance statistics.
20
21The script accepts flexible stage configurations via CLI arguments, where each
22stage can have:
23- Custom processing time (sleep duration in seconds)
24- Custom concurrency level (number of parallel tasks)
25- Optional aggregation (batch size for grouping items)
26
27Performance statistics are collected at configurable intervals and saved to
28a SQLite database for analysis. The foreground thread simulates a consumer
29(e.g., training loop) with configurable sleep duration.
30
31Example usage:
32
33.. code-block::
34
35 # Single stage with 50ms processing
36 python performance_simulation.py --db-path output.db --stage-configs "0.050,1"
37
38 # Three stages: fast (10ms) -> medium (25ms) -> slow (40ms)
39 python performance_simulation.py --db-path output.db --stage-configs "0.010,1;0.025,1;0.040,1"
40
41 # Stage with concurrency and aggregation
42 python performance_simulation.py --db-path output.db --stage-configs "0.006,1,1;0.0,1,4"
43"""
44
45import argparse
46import asyncio
47import logging
48import random
49import time
50from collections.abc import Iterable
51from functools import partial
52from pathlib import Path
53from queue import Queue
54from typing import Any, TypeVar
55
56from spdl.pipeline import Pipeline, PipelineBuilder, StageInfo
57
58try:
59 from examples.sqlite_stats_logger import ( # pyre-ignore[21]
60 EventLogEntry,
61 log_stats_summary,
62 QueueStatsLogEntry,
63 SQLiteStatsWriter,
64 TaskStatsLogEntry,
65 )
66except ImportError:
67 from spdl.examples.sqlite_stats_logger import (
68 EventLogEntry,
69 log_stats_summary,
70 QueueStatsLogEntry,
71 SQLiteStatsWriter,
72 TaskStatsLogEntry,
73 )
74
75try:
76 from examples.performance_analysis import ( # pyre-ignore[21]
77 StatsQueueWithLogging,
78 TaskStatsHookWithLogging,
79 )
80except ImportError:
81 from spdl.examples.performance_analysis import (
82 StatsQueueWithLogging,
83 TaskStatsHookWithLogging,
84 )
85
86
87__all__ = [
88 "parse_args",
89 "main",
90 "build_pipeline",
91 "SimulatedStage",
92]
93
94_LG: logging.Logger = logging.getLogger(__name__)
95
96T = TypeVar("T")
97
98
99class SimulatedStage:
100 """Callable stage with configurable sleep duration.
101
102 This class simulates a processing stage with a specified average
103 sleep duration. Each invocation applies ±10% random jitter to the
104 sleep duration to simulate processing time variance.
105
106 Args:
107 sleep_duration: Average sleep duration in seconds (e.g., 0.050 for 50ms).
108 Actual duration will vary by ±10% on each call.
109 """
110
111 def __init__(self, sleep_duration: float) -> None:
112 self.sleep_duration = sleep_duration
113
114 async def __call__(self, item: T) -> T:
115 """Process an item with simulated delay including random jitter.
116
117 Applies the configured sleep duration with ±10% random jitter to simulate
118 realistic processing time variance.
119
120 Args:
121 item: The item to process. (The data is passed through unmodified)
122
123 Returns:
124 The same item (unmodified) after simulated processing delay.
125 """
126 if self.sleep_duration > 0:
127 # Add ±10% jitter to make simulation more realistic
128 jitter = random.uniform(-0.1, 0.1)
129 actual_duration = self.sleep_duration * (1 + jitter)
130 await asyncio.sleep(actual_duration)
131 return item
132
133
134def build_pipeline(
135 source: Iterable[int],
136 log_interval: float,
137 buffer: Queue[TaskStatsLogEntry | QueueStatsLogEntry | EventLogEntry],
138 stage_configs: list[tuple[float, int, int]],
139) -> Pipeline:
140 """Build a pipeline with configurable stage functions and aggregation.
141
142 Creates a pipeline with one or more stages, each having configurable processing
143 time, concurrency, and optional aggregation. Stage names are automatically
144 generated based on their configuration (e.g., "25ms", "pass", "50ms_c2").
145
146 Args:
147 source: A data source (iterable) containing integers.
148 log_interval: The interval in seconds at which performance statistics are logged.
149 buffer: Shared queue for collecting :py:class:`TaskStatsLogEntry`, :py:class`QueueStatsLogEntry`,
150 and :py:class:`EventLogEntry` objects.
151 stage_configs: List of tuples (sleep_duration, concurrency, batch_size) for each stage.
152
153 - ``sleep_duration``: Processing time in seconds (0 for passthrough)
154 - ``concurrency``: Number of parallel tasks (>= 1)
155 - ``batch_size``: Number of items to aggregate (1 for no aggregation)
156
157 Example: ``[(0.050, 1, 1), (0.025, 2, 1), (0.0, 1, 4)]`` creates three stages
158 where the last stage aggregates 4 items into batches.
159
160 Returns:
161 A configured Pipeline instance ready for execution with performance monitoring.
162 """
163
164 def hook_factory(info: StageInfo) -> list[Any]:
165 return [
166 TaskStatsHookWithLogging(
167 info,
168 buffer=buffer,
169 interval=log_interval,
170 )
171 ]
172
173 builder = PipelineBuilder().add_source(source=source)
174
175 # Calculate total num_threads as the sum of all stage concurrencies
176 total_threads = sum(concurrency for _, concurrency, _ in stage_configs)
177
178 # Add stages based on configuration with descriptive names
179 for sleep_duration, concurrency, batch_size in stage_configs:
180 stage = SimulatedStage(sleep_duration)
181
182 # Create short, descriptive name based on configuration
183 if sleep_duration == 0:
184 name = "pass"
185 else:
186 sleep_ms = int(sleep_duration * 1000)
187 name = f"{sleep_ms}ms"
188
189 # Add concurrency suffix if > 1
190 if concurrency > 1:
191 name = f"{name}_c{concurrency}"
192
193 builder = builder.pipe(stage, concurrency=concurrency, name=name)
194
195 # If batch_size > 1, insert aggregation after the processing stage
196 if batch_size > 1:
197 builder = builder.aggregate(batch_size)
198
199 return builder.add_sink().build(
200 num_threads=total_threads,
201 queue_class=partial( # pyre-ignore[6]
202 StatsQueueWithLogging,
203 buffer=buffer,
204 interval=log_interval,
205 ),
206 task_hook_factory=hook_factory,
207 )
208
209
210def infinite_source() -> Iterable[int]:
211 """Generate an infinite stream of integers."""
212 counter = 0
213 while True:
214 yield counter
215 counter += 1
216
217
218def run_configuration(
219 db_path: Path,
220 stage_configs: list[tuple[float, int, int]],
221 log_interval: float,
222 run_duration: float,
223 foreground_sleep: float,
224) -> None:
225 """Run a single pipeline configuration and collect performance statistics.
226
227 Builds and runs a pipeline with the specified configuration, simulating a
228 foreground consumer (e.g., training loop) that processes items with a
229 configurable sleep duration. Performance statistics are collected and saved
230 to a SQLite database.
231
232 Args:
233 db_path: Path where the SQLite database file will be saved.
234 stage_configs: List of tuples (sleep_duration, concurrency, batch_size) defining
235 each pipeline stage's configuration.
236 log_interval: Interval in seconds at which performance statistics are logged
237 to the database.
238 run_duration: Total duration in seconds to run the pipeline before stopping.
239 foreground_sleep: Sleep duration in seconds for the foreground consumer thread
240 (e.g., 0.030 for 30ms).
241 """
242 print(f"\n{'=' * 80}")
243 print("🎯 Running Pipeline Simulation")
244 print(f"{'=' * 80}")
245 print(f" Database: {db_path}")
246 print(f" Stage configs: {stage_configs}")
247 print(f" Log interval: {log_interval}s")
248 print(f" Run duration: {run_duration}s")
249 print(f" Foreground sleep: {foreground_sleep * 1000:.0f}ms")
250 print()
251
252 # Create shared buffer and writer
253 buffer: Queue[TaskStatsLogEntry | QueueStatsLogEntry | EventLogEntry] = Queue()
254 writer = SQLiteStatsWriter(
255 str(db_path),
256 buffer,
257 flush_interval=max(0.1, log_interval - 1),
258 )
259 writer.start()
260
261 # Build pipeline
262 pipeline = build_pipeline(
263 source=infinite_source(),
264 log_interval=log_interval,
265 buffer=buffer,
266 stage_configs=stage_configs,
267 )
268
269 try:
270 start_time = time.monotonic()
271 item_count = 0
272
273 with pipeline.auto_stop():
274 for _ in pipeline:
275 item_count += 1
276
277 # Simulate foreground work (e.g., training loop)
278 time.sleep(foreground_sleep)
279
280 # Check if we've run long enough
281 elapsed = time.monotonic() - start_time
282 if elapsed >= run_duration:
283 break
284
285 elapsed = time.monotonic() - start_time
286 print(f"\n✅ Configuration completed in {elapsed:.2f} seconds")
287 print(f" Items processed: {item_count}")
288 print(f" Average throughput: {item_count / elapsed:.2f} items/sec")
289
290 finally:
291 # Ensure all stats are flushed to database
292 writer.shutdown()
293
294 # Log stats summary
295 log_stats_summary(db_path)
296
297
298def parse_args() -> argparse.Namespace:
299 """Parse command line arguments for pipeline simulation configuration.
300
301 Returns:
302 Parsed arguments including database path, stage configurations,
303 log interval, run duration, and foreground sleep time.
304 """
305 parser = argparse.ArgumentParser(
306 description=__doc__,
307 formatter_class=argparse.RawDescriptionHelpFormatter,
308 )
309 parser.add_argument(
310 "--db-path",
311 type=Path,
312 required=True,
313 help="Path to save the SQLite database file",
314 )
315 parser.add_argument(
316 "--stage-configs",
317 type=str,
318 required=True,
319 help='Stage configurations as "sleep1,concurrency1;sleep2,concurrency2;..." (e.g., "0.050,1;0.030,1;0.010,1")',
320 )
321 parser.add_argument(
322 "--log-interval",
323 type=float,
324 default=1.0,
325 help="Interval in seconds for logging stats to database (default: 1)",
326 )
327 parser.add_argument(
328 "--run-duration",
329 type=float,
330 default=10.0,
331 help="Duration in seconds to run the configuration (default: 10)",
332 )
333 parser.add_argument(
334 "--foreground-sleep",
335 type=float,
336 default=0.030,
337 help="Sleep duration in foreground thread in seconds (default: 0.030 = 30ms)",
338 )
339 return parser.parse_args()
340
341
342def parse_stage_configs(config_str: str) -> list[tuple[float, int, int]]:
343 """Parse stage configuration string into list of tuples.
344
345 Args:
346 config_str: Configuration string like "0.050,1,1;0.030,1,4;0.010,1,1;" where each stage
347 has format "sleep,concurrency,batch_size". Batch_size is optional (defaults to 1).
348 Examples:
349 - "0.050,1;0.030,1" - two stages with batch_size=1 (no aggregation)
350 - "0.050,1,1;0.030,1,4" - stage 1 aggregates 4 items into 1
351
352 Returns:
353 List of (sleep_duration, concurrency, batch_size) tuples
354
355 Raises:
356 ValueError: If the configuration string is invalid
357 """
358 try:
359 stages = []
360 for stage_str in config_str.split(";"):
361 stage_str = stage_str.strip()
362 if not stage_str:
363 continue
364 parts = stage_str.split(",")
365 if len(parts) < 2 or len(parts) > 3:
366 raise ValueError(
367 f"Invalid stage format '{stage_str}'. Expected 'sleep,concurrency[,batch_size]'"
368 )
369 sleep_duration = float(parts[0])
370 concurrency = int(parts[1])
371 batch_size = int(parts[2]) if len(parts) == 3 else 1
372
373 if sleep_duration < 0:
374 raise ValueError(
375 f"Sleep duration must be non-negative: {sleep_duration}"
376 )
377 if concurrency < 1:
378 raise ValueError(f"Concurrency must be at least 1: {concurrency}")
379 if batch_size < 1:
380 raise ValueError(f"Batch size must be at least 1: {batch_size}")
381
382 stages.append((sleep_duration, concurrency, batch_size))
383 if not stages:
384 raise ValueError("At least one stage configuration is required")
385 return stages
386 except (ValueError, IndexError) as e:
387 raise ValueError(
388 f"Invalid stage configuration '{config_str}': {e}. "
389 "Expected format: 'sleep1,concurrency1[,batch_size1];sleep2,concurrency2[,batch_size2];...'"
390 ) from e
391
392
393def main() -> None:
394 """Main entry point for the pipeline performance simulation.
395
396 Parses command line arguments, builds a pipeline with the specified
397 configuration, runs the simulation, and saves performance statistics
398 to a SQLite database.
399 """
400 logging.basicConfig(
401 level=logging.INFO,
402 format="%(asctime)s [%(levelname).1s]: %(message)s",
403 )
404
405 args = parse_args()
406
407 # Parse stage configurations from CLI
408 try:
409 stage_configs = parse_stage_configs(args.stage_configs)
410 except ValueError as e:
411 print(f"Error: {e}")
412 return
413
414 # Ensure output directory for database exists
415 args.db_path.parent.mkdir(parents=True, exist_ok=True)
416
417 print("\n🚀 Pipeline Bottleneck Simulation")
418 print(f" Database file: {args.db_path}")
419 print(f" Stage configs: {stage_configs}")
420 print()
421
422 # Run the configuration
423 run_configuration(
424 db_path=args.db_path,
425 stage_configs=stage_configs,
426 log_interval=args.log_interval,
427 run_duration=args.run_duration,
428 foreground_sleep=args.foreground_sleep,
429 )
430
431 print("\n" + "=" * 80)
432 print("🎉 Simulation completed!")
433 print("=" * 80)
434 print(f"\nDatabase file created: {args.db_path}")
435 print()
436
437
438if __name__ == "__main__":
439 main()
API Reference¶
Functions
- parse_args() Namespace[source]¶
Parse command line arguments for pipeline simulation configuration.
- Returns:
Parsed arguments including database path, stage configurations, log interval, run duration, and foreground sleep time.
- main() None[source]¶
Main entry point for the pipeline performance simulation.
Parses command line arguments, builds a pipeline with the specified configuration, runs the simulation, and saves performance statistics to a SQLite database.
- build_pipeline(source: Iterable[int], log_interval: float, buffer: Queue[TaskStatsLogEntry | QueueStatsLogEntry | EventLogEntry], stage_configs: list[tuple[float, int, int]]) Pipeline[source]¶
Build a pipeline with configurable stage functions and aggregation.
Creates a pipeline with one or more stages, each having configurable processing time, concurrency, and optional aggregation. Stage names are automatically generated based on their configuration (e.g., “25ms”, “pass”, “50ms_c2”).
- Parameters:
source – A data source (iterable) containing integers.
log_interval – The interval in seconds at which performance statistics are logged.
buffer – Shared queue for collecting
TaskStatsLogEntry, :py:class`QueueStatsLogEntry`, andEventLogEntryobjects.stage_configs –
List of tuples (sleep_duration, concurrency, batch_size) for each stage.
sleep_duration: Processing time in seconds (0 for passthrough)concurrency: Number of parallel tasks (>= 1)batch_size: Number of items to aggregate (1 for no aggregation)
Example:
[(0.050, 1, 1), (0.025, 2, 1), (0.0, 1, 4)]creates three stages where the last stage aggregates 4 items into batches.
- Returns:
A configured Pipeline instance ready for execution with performance monitoring.
Classes
- class SimulatedStage(sleep_duration: float)[source]¶
Callable stage with configurable sleep duration.
This class simulates a processing stage with a specified average sleep duration. Each invocation applies ±10% random jitter to the sleep duration to simulate processing time variance.
- Parameters:
sleep_duration – Average sleep duration in seconds (e.g., 0.050 for 50ms). Actual duration will vary by ±10% on each call.