Jump to content

RidgeRun Developer Manual - Profiling Tools - Pipeline Latency

From RidgeRun Developer Wiki

Follow us on: YouTube Twitter LinkedIn Email Share this page

Share This Page



  Index Next: Methodologies





Obtaining the latency of the element from a pipeline

The RidgeRun team has developed a Python Script that parses and summarizes latency measurements obtained from a Gstreamer pipeline. The idea behind it is to provide a tool capable of profiling pipelines' latency through each element individually. This statistics include mean, min/max, and percentiles to quantify the overall latency contribution across the pipeline being measured.

Find the Python Script here

#!/usr/bin/env python3

"""Parse GStreamer latency logs from tracers.

The logs are generated with the following options:
    GST_DEBUG_NO_COLOR=1 GST_DEBUG=GST_TRACER:7 GST_TRACERS="latency(flags=element)"

Computes latency statistics per element:
  count, avg, min, max, p50, p90, p95, p99

Additionally prints:
  TOTAL_AVG_SUM_MS  -> sum of per-element average latency (in ms)
  TOTAL_AVG_SUM_NS  -> same value in nanoseconds

Assumes `time=(guint64)N` is in nanoseconds (ns).

"""

# ruff: noqa: T201

from __future__ import annotations

import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path

LINE_RE = re.compile(
    r"element-latency,.*?element=\(string\)(?P<element>[^,]+),.*?time=\(guint64\)(?P<time>\d+)",
    re.IGNORECASE,
)


@dataclass
class Stats:
    """Latency statistics."""

    count: int = 0
    total_ns: int = 0
    min_ns: int | None = None
    max_ns: int | None = None
    samples_ns: list[int] | None = None


def _safe_int(value: str) -> int:
    try:
        return int(value)
    except ValueError as exc:
        msg = f"Invalid integer value: {value!r}"
        raise ValueError(msg) from exc


def _add_sample(stats: Stats, value_ns: int, *, keep_samples: bool) -> None:
    stats.count += 1
    stats.total_ns += value_ns

    if stats.min_ns is None or value_ns < stats.min_ns:
        stats.min_ns = value_ns
    if stats.max_ns is None or value_ns > stats.max_ns:
        stats.max_ns = value_ns

    if keep_samples:
        if stats.samples_ns is None:
            stats.samples_ns = []
        stats.samples_ns.append(value_ns)


def _percentile(sorted_values: list[int], p: float) -> int:
    if not sorted_values:
        msg = "Cannot compute percentile of empty list"
        raise ValueError(msg)

    largest_percentile = 100
    if p <= 0:
        return sorted_values[0]
    if p >= largest_percentile:
        return sorted_values[-1]

    n = len(sorted_values)
    rank = int((p / largest_percentile) * n)
    if rank <= 0:
        return sorted_values[0]
    if rank >= n:
        return sorted_values[-1]
    return sorted_values[rank - 1]


def _ns_to_ms(ns: int) -> float:
    return ns / 1_000_000.0


def _parse_file(path: Path, *, keep_samples: bool) -> dict[str, Stats]:
    per_element: dict[str, Stats] = {}

    with path.open("r", encoding="utf-8", errors="replace") as f:
        for line in f:
            match = LINE_RE.search(line)
            if not match:
                continue

            element = match.group("element").strip()
            time_ns = _safe_int(match.group("time"))

            stats = per_element.get(element)
            if stats is None:
                stats = Stats(samples_ns=[] if keep_samples else None)
                per_element[element] = stats

            _add_sample(stats, time_ns, keep_samples=keep_samples)

    return per_element


def _average_ns(stats: Stats) -> int:
    if stats.count == 0:
        return 0
    return stats.total_ns // stats.count


def _format_row(
    element: str,
    stats: Stats,
    *,
    with_percentiles: bool,
) -> str:
    avg_ns = _average_ns(stats)
    min_ns = stats.min_ns if stats.min_ns is not None else 0
    max_ns = stats.max_ns if stats.max_ns is not None else 0

    if with_percentiles:
        values = sorted(stats.samples_ns or [])
        p50 = _percentile(values, 50)
        p90 = _percentile(values, 90)
        p95 = _percentile(values, 95)
        p99 = _percentile(values, 99)

        return (
            f"{element:<28} "
            f"{stats.count:>8} "
            f"{_ns_to_ms(avg_ns):>12.3f} "
            f"{_ns_to_ms(min_ns):>12.3f} "
            f"{_ns_to_ms(max_ns):>12.3f} "
            f"{_ns_to_ms(p50):>12.3f} "
            f"{_ns_to_ms(p90):>12.3f} "
            f"{_ns_to_ms(p95):>12.3f} "
            f"{_ns_to_ms(p99):>12.3f}"
        )

    return (
        f"{element:<28} "
        f"{stats.count:>8} "
        f"{_ns_to_ms(avg_ns):>12.3f} "
        f"{_ns_to_ms(min_ns):>12.3f} "
        f"{_ns_to_ms(max_ns):>12.3f}"
    )


def main(argv: list[str]) -> int:
    """Compute average element latency from GStreamer tracer logs."""
    parser = argparse.ArgumentParser(
        description="Compute average element latency from GStreamer tracer logs."
    )
    parser.add_argument("logfile", type=Path, help="Path to the tracer log file")
    parser.add_argument(
        "--no-percentiles",
        action="store_true",
        help="Disable percentile computation.",
    )
    parser.add_argument(
        "--sort",
        choices=["name", "avg", "count", "max"],
        default="avg",
        help="Sort output by this key (default: avg).",
    )

    args = parser.parse_args(argv)

    with_percentiles = not args.no_percentiles
    per_element = _parse_file(args.logfile, keep_samples=with_percentiles)

    if not per_element:
        print("No element-latency entries found.", file=sys.stderr)
        return 2

    def sort_key(item: tuple[str, Stats]) -> str | int:
        element, stats = item
        if args.sort == "name":
            return element
        if args.sort == "count":
            return stats.count
        if args.sort == "max":
            return stats.max_ns or 0
        return _average_ns(stats)

    items = sorted(per_element.items(), key=sort_key, reverse=(args.sort != "name"))

    if with_percentiles:
        header = (
            f"{'element':<28} {'count':>8} "
            f"{'avg_ms':>12} {'min_ms':>12} {'max_ms':>12} "
            f"{'p50_ms':>12} {'p90_ms':>12} {'p95_ms':>12} {'p99_ms':>12}"
        )
    else:
        header = (
            f"{'element':<28} {'count':>8} {'avg_ms':>12} {'min_ms':>12} {'max_ms':>12}"
        )

    print(header)
    print("-" * len(header))

    total_avg_sum_ns = 0

    for element, stats in items:
        avg_ns = _average_ns(stats)
        total_avg_sum_ns += avg_ns

        row = _format_row(element, stats, with_percentiles=with_percentiles)
        print(row)

    print("-" * len(header))
    print(f"{'TOTAL_AVG_SUM_MS':<28} {'':>8} {_ns_to_ms(total_avg_sum_ns):>12.3f}")
    print(f"{'TOTAL_AVG_SUM_NS':<28} {'':>8} {total_avg_sum_ns:>12}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))

How to Use the Tool

1. Define and Run the pipeline

Set up a Gstreamer pipeline with a fixed number of buffers to ensure controlled execution. The pipeline will run with a latency tracer enabled which will then be dumped into a log file named elements_latency.log that serves as the input for the Python script. Feel free to change the running pipeline as you need.

# Limit execution using num-buffers
# This can also be replaced with a custom GStreamer application
PIPELINE="gst-launch-1.0 videotestsrc is-live=1 num-buffers=300 ! videoconvert ! fakesink"

# Run with latency tracer enabled
GST_DEBUG_NO_COLOR=1 \
GST_DEBUG=GST_TRACER:7 \
GST_TRACERS="latency(flags=element)" \
eval "$PIPELINE" 2> elements_latency.log

2. Parse the Results

Run the parser script with the elements_latency.log as an input file to process and compute latency statistics:

python3 gst_tracer_latency_log_parser.py elements_latency.log

Expected Output

The script will print in terminal a table summarizing latency metrics for each pipeline element:

element                         count       avg_ms       min_ms       max_ms       p50_ms       p90_ms       p95_ms       p99_ms
--------------------------------------------------------------------------------------------------------------------------------
videoconvert0                     300        0.045        0.016        0.278        0.044        0.064        0.073        0.092
--------------------------------------------------------------------------------------------------------------------------------
TOTAL_AVG_SUM_MS                             0.045
TOTAL_AVG_SUM_NS                             45346
  • Per-element metrics: Include count, average latency, min/max latency, and percentile distributions
  • TOTAL_AVG_SUM: Includes the sum of average latencies for each of the pipeline's elements, providing an estimate of the overall processing latency.


Previous: Profiling_Tools/Linux_Perf Index Next: Methodologies



Cookies help us deliver our services. By using our services, you agree to our use of cookies.