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`, and EventLogEntry objects.

  • 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.