blob: caeef8ed6f6fd974a0d0038964a8901c13404b7a [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2024 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.
"""Scenic trace metrics."""
import logging
import statistics
from typing import MutableSequence, Tuple
from reporting import metrics
from trace_processing import trace_metrics, trace_model, trace_utils
_LOGGER: logging.Logger = logging.getLogger("ScenicMetricsProcessor")
_EVENT_CATEGORY: str = "gfx"
_SCENIC_START_EVENT_NAME: str = "ApplyScheduledSessionUpdates"
_SCENIC_RENDER_EVENT_NAME: str = "RenderFrame"
_DISPLAY_VSYNC_READY_EVENT_NAME: str = "Flatland::DisplayCompositor::OnVsync"
_PREP_AND_RENDER_FLOW_NAME: str = "scenic_frame"
_RENDER_FLOW_NAME: str = "render_frame_to_vsync"
class _ScenicTracingEvent:
start_event: trace_model.Event
render_event: trace_model.DurationEvent
vsync_event: trace_model.Event
def __init__(
self,
start_event: trace_model.Event,
render_event: trace_model.DurationEvent,
vsync_event: trace_model.Event,
):
self.start_event = start_event
self.render_event = render_event
self.vsync_event = vsync_event
class ScenicMetricsProcessor(trace_metrics.MetricsProcessor):
"""Computes CPU and GPU time spent rendering frames in Scenic.
Calculates total time-to-render-frame by measuring from the moment that Scenic reports it has
begun computing frame contents to the moment that is ready for vsync. Also tracks time spent on
CPU-bound operations for each frame. Flow events in the trace enable this class to reliably
correlate the correct events to calculate this duration.
By default, this module reports aggregate latency measurements -- such as min, max, average, and
percentiles -- calculated across all frames rendered during the test. It can be
configured to instead report a time series of measurements, one for each event.
"""
def __init__(self, aggregates_only: bool = True):
"""Constructor.
Args:
aggregates_only: When True, generates RenderCpu[Min|Max|Average|P*] and
RenderTotal[Min|Max|Average|P*].
Otherwise generates RenderCpu and RenderTotal with the raw values.
"""
self.aggregates_only: bool = aggregates_only
@property
def event_patterns(self) -> set[str]:
"""Patterns describing the trace events needed to generate these metrics."""
return {
_SCENIC_START_EVENT_NAME,
_SCENIC_RENDER_EVENT_NAME,
_DISPLAY_VSYNC_READY_EVENT_NAME,
_PREP_AND_RENDER_FLOW_NAME,
_RENDER_FLOW_NAME,
}
def process_metrics(
self, model: trace_model.Model
) -> MutableSequence[metrics.TestCaseResult]:
# This method looks for a possible race between trace event start in Scenic and magma.
# We can safely skip these events. See https://fxbug.dev/322849857 for more details.
model = trace_utils.adjust_to_common_process_start(
model,
_SCENIC_START_EVENT_NAME,
category=_EVENT_CATEGORY,
type=trace_model.DurationEvent,
)
# Since `filter_events()` returns a Generator, make a local copy so we can iterate over the
# events more than once.
scenic_start_events = list(
trace_utils.filter_events(
model.all_events(),
category=_EVENT_CATEGORY,
name=_SCENIC_START_EVENT_NAME,
type=trace_model.DurationEvent,
)
)
tracing_events: list[_ScenicTracingEvent] = []
for e in scenic_start_events:
render_event = trace_utils.get_nearest_following_flow_event(
e, _EVENT_CATEGORY, _SCENIC_RENDER_EVENT_NAME
)
if render_event is None:
continue
assert isinstance(render_event, trace_model.DurationEvent)
if not render_event.duration:
continue
vsync_ready_event = trace_utils.get_nearest_following_flow_event(
render_event, _EVENT_CATEGORY, _DISPLAY_VSYNC_READY_EVENT_NAME
)
if vsync_ready_event is None:
continue
tracing_events.append(
_ScenicTracingEvent(e, render_event, vsync_ready_event)
)
if len(tracing_events) < 1:
_LOGGER.warning(
"No render or vsync events are present. Perhaps the trace "
"duration is too short to provide scenic render information"
)
return []
cpu_render_times: list[float] = []
for tracing_event in tracing_events:
if tracing_event.render_event.duration is None:
continue
cpu_render_times.append(
(
tracing_event.render_event.start
+ tracing_event.render_event.duration
- tracing_event.start_event.start
).to_milliseconds_f()
)
cpu_render_mean: float = statistics.mean(cpu_render_times)
_LOGGER.info(f"Average CPU render time: {cpu_render_mean} ms")
total_render_times: list[float] = []
for tracing_event in tracing_events:
(
tracing_event.vsync_event.start
- tracing_event.start_event.start
).to_milliseconds_f()
total_render_times.append(
(
tracing_event.vsync_event.start
- tracing_event.start_event.start
).to_milliseconds_f()
)
total_render_mean: float = statistics.mean(total_render_times)
_LOGGER.info(f"Average Total render time: {total_render_mean} ms")
metrics_list: list[Tuple[str, list[float]]] = [
("RenderCpu", cpu_render_times),
("RenderTotal", total_render_times),
]
test_case_results: list[metrics.TestCaseResult] = []
for name, values in metrics_list:
if self.aggregates_only:
test_case_results.extend(
trace_utils.standard_metrics_set(
values=values,
label_prefix=name,
unit=metrics.Unit.milliseconds,
)
)
else:
test_case_results.append(
metrics.TestCaseResult(
name, metrics.Unit.milliseconds, values
)
)
return test_case_results