blob: bf3cacf1755538d26374f8878b20df6ed2fdf4c3 [file]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Scheduler trace metrics."""
import dataclasses
import enum
import logging
import typing
from collections.abc import MutableSequence
from reporting import metrics
from trace_processing import trace_metrics, trace_model, trace_time, trace_utils
_LOGGER: logging.Logger = logging.getLogger("SchedulerMetricsProcessor")
_PREEMPTED_STATES: frozenset[trace_model.ThreadState] = frozenset(
{
trace_model.ThreadState.ZX_THREAD_STATE_NEW,
trace_model.ThreadState.ZX_THREAD_STATE_RUNNING,
}
)
class _WaitKind(enum.Enum):
WAKEUP = 1
PREEMPTION = 2
@dataclasses.dataclass(frozen=True)
class _PendingWait:
kind: _WaitKind
start: trace_time.TimePoint
class SchedulerMetricsProcessor(trace_metrics.MetricsProcessor):
"""Computes kernel scheduler task wait-time metrics (wakeup latency and preemption delay).
Calculates system-wide distributions of:
- SchedWakeupLatency: Time from a thread waking up (`Waking` record) until it is scheduled
onto a CPU (`ContextSwitch` record).
- SchedPreemptionDelay: Time a runnable thread that was preempted (`ContextSwitch` with
`outgoing_state` indicating it remains runnable) waits before being scheduled back onto
a CPU.
- SchedTaskWaitTime: Combined distribution of runnable wait times (both wakeup latencies
and preemption delays).
Idle threads and synthetic priority-inheritance self-switches (`incoming_tid == outgoing_tid`)
are excluded from all calculations.
"""
def __init__(self, aggregates_only: bool = False) -> None:
"""Constructor.
Args:
aggregates_only: When True, generates Min, Max, Average, and P* (percentiles)
via `standard_metrics_set`. Otherwise generates raw value series for
SchedWakeupLatency, SchedPreemptionDelay, and SchedTaskWaitTime.
"""
self._aggregates_only: bool = aggregates_only
@property
@typing.override
def event_patterns(self) -> set[str]:
"""Scheduler records are always present in the model; no named events are required."""
return set()
@typing.override
def process_metrics(
self, model: trace_model.Model
) -> MutableSequence[metrics.TestCaseResult]:
if not model.scheduling_records:
_LOGGER.warning(
"No scheduling records are present in the trace model."
)
return []
# Seed with TID 0, which the FXT importer uses for synthetic idle states
# (including a CPU's initial ContextSwitch where outgoing_prio is None).
idle_tids: set[int] = {0}
all_records: list[trace_model.SchedulingRecord] = []
for records in model.scheduling_records.values():
for record in records:
if record.is_idle():
idle_tids.add(record.tid)
if (
isinstance(record, trace_model.ContextSwitch)
and record.outgoing_prio == trace_model.INT32_MIN
):
idle_tids.add(record.outgoing_tid)
all_records.append(record)
# Sort chronologically across all CPUs. For identical timestamps, process Waking
# records before ContextSwitch records so zero-latency wakeups are captured.
all_records.sort(
key=lambda r: (
r.start,
0 if isinstance(r, trace_model.Waking) else 1,
)
)
pending_waits: dict[int, _PendingWait] = {}
wakeup_latencies: list[float] = []
preemption_delays: list[float] = []
task_wait_times: list[float] = []
for record in all_records:
if isinstance(record, trace_model.Waking):
if record.tid not in idle_tids:
pending_waits[record.tid] = _PendingWait(
kind=_WaitKind.WAKEUP, start=record.start
)
elif isinstance(record, trace_model.ContextSwitch):
# Ignore synthetic self-switches emitted during priority-inheritance updates.
if record.tid == record.outgoing_tid:
continue
in_tid = record.tid
pending = pending_waits.pop(in_tid, None)
if pending is not None and record.start >= pending.start:
wait_ms = (record.start - pending.start).to_milliseconds_f()
if pending.kind == _WaitKind.WAKEUP:
wakeup_latencies.append(wait_ms)
else:
preemption_delays.append(wait_ms)
task_wait_times.append(wait_ms)
out_tid = record.outgoing_tid
if out_tid not in idle_tids:
if record.outgoing_state in _PREEMPTED_STATES:
pending_waits[out_tid] = _PendingWait(
kind=_WaitKind.PREEMPTION, start=record.start
)
else:
# The thread blocked or terminated; discard any stale pending wait such as one left
# behind when its switch-in record was dropped.
pending_waits.pop(out_tid, None)
metric_series: list[tuple[str, list[float], str]] = [
(
"SchedWakeupLatency",
wakeup_latencies,
"Time from thread wakeup until scheduled on a CPU",
),
(
"SchedPreemptionDelay",
preemption_delays,
"Time a preempted runnable thread waits until next scheduled on a CPU",
),
(
"SchedTaskWaitTime",
task_wait_times,
"Total runnable wait time (wakeup latency and preemption delay) before being scheduled on a CPU",
),
]
results: list[metrics.TestCaseResult] = []
for label_prefix, values, doc_prefix in metric_series:
if not values:
_LOGGER.info(f"No {label_prefix} samples found in trace.")
continue
if self._aggregates_only:
results.extend(
trace_utils.standard_metrics_set(
values=values,
label_prefix=label_prefix,
unit=metrics.Unit.milliseconds,
doc_prefix=doc_prefix,
)
)
else:
results.append(
metrics.TestCaseResult(
label=label_prefix,
unit=metrics.Unit.milliseconds,
values=values,
doc=doc_prefix,
)
)
return results