From ad437c206101521f063393d1defc6747f8c39d1a Mon Sep 17 00:00:00 2001 From: Alex Jones Date: Wed, 13 May 2026 16:12:26 +0100 Subject: [PATCH] feat: add instrumentation graphs of the longest jobs & tests per block Add two more visualizations to the instrumentation report - one showing the longest jobs per block (variant), and another showing the longest tests per block (variant). As with the per-tool graphs, these are only shown if appropriate metadata exists. Since the test grouping logic was added in the previous commit, this is implemented by just adding a couple of extra subclasses with a relevant categorization function and applicable scaling parameters applied. In this case, we expect to have a lot more blocks than tools, so we have lower bars being rendered by default. Signed-off-by: Alex Jones --- src/dvsim/instrumentation/report/longest.py | 90 ++++++++++++++++++++ src/dvsim/instrumentation/report/registry.py | 4 + 2 files changed, 94 insertions(+) diff --git a/src/dvsim/instrumentation/report/longest.py b/src/dvsim/instrumentation/report/longest.py index 3bd02d7f..a1537c1d 100644 --- a/src/dvsim/instrumentation/report/longest.py +++ b/src/dvsim/instrumentation/report/longest.py @@ -597,3 +597,93 @@ def for_profile(cls, profile: RenderProfile) -> Self: ) return cls(max_bars=None, max_jobs_per_bar=None) return cls() + + +class LongestByBlockChart(LongestBarChart): + """Renders plotly bar charts that rank the longest jobs or tests, partitioned by block.""" + + title = "Longest Jobs by Block" + + DEFAULT_MAX_BARS: int = 50 + + def __init__( + self, + *, + group_tests: bool = False, + max_bars: int | None = DEFAULT_MAX_BARS, + max_jobs_per_bar: int | None = None, + ) -> None: + """Construct a LongestByBlockChart. + + Args: + group_tests: Flag to group jobs of the same test by name. This allows timing results + for the same tests to be combined in a stacked bar view. + max_bars: The maximum number of bars to show for each block (and overall). + max_jobs_per_bar: If group_tests=True, the maximum number of stacked bars to + render per bar. If there are too many bars to display, the bottom + (N - max_jobs_per_bar + 1) jobs will be combined into a single bar. + + """ + super().__init__( + max_bars=max_bars, + max_jobs_per_bar=max_jobs_per_bar, + category_fn=self._get_job_block_variant, + group_tests=group_tests, + ) + + def _get_job_block_variant(self, job: JobInstrumentationResults) -> str: + """Get the block (variant) from a job's metadata, or 'Unknown' if it does not exist.""" + if job.meta is None: + return "Unknown" + + return job.meta.block + (f"_{job.meta.block_variant}" if job.meta.block_variant else "") + + +class LongestTestsByBlockChart(LongestByBlockChart): + """Renders plotly bar charts that rank the longest tests, partitioned by block.""" + + title = "Longest Tests by Block" + + DEFAULT_MAX_BARS: int = 20 + + def __init__( + self, + *, + max_bars: int | None = DEFAULT_MAX_BARS, + max_jobs_per_bar: int | None = LongestByBlockChart.DEFAULT_MAX_JOBS_PER_BAR, + ) -> None: + """Construct a LongestTestsByBlockChart. + + Args: + max_bars: The maximum number of bars to show for each block (and overall). + max_jobs_per_bar: The maximum number of stacked bars to render per bar. If there are + too many bars to display, the bottom (N - max_jobs_per_bar + 1) jobs will be combined + into a single bar. + + """ + super().__init__(group_tests=True, max_bars=max_bars, max_jobs_per_bar=max_jobs_per_bar) + + @classmethod + def for_profile(cls, profile: RenderProfile) -> Self: + """Create a visualizer instance configured for a given rendering profile.""" + if profile == RenderProfile.HIGH: + max_bars = cls.DEFAULT_MAX_BARS * 5 + max_jobs_per_bar = (cls.DEFAULT_MAX_JOBS_PER_BAR - 1) * 5 + 1 + log.debug( + "Using render profile '%s' for '%s' visualization. Setting max bars to %d " + "and max jobs per bar to %d.", + profile.name, + cls.title, + max_bars, + max_jobs_per_bar, + ) + return cls(max_bars=max_bars, max_jobs_per_bar=max_jobs_per_bar) + if profile == RenderProfile.FULL: + log.debug( + "Using render profile '%s' for '%s' visualization. Disabling max bars and " + "max jobs per bar.", + profile.name, + cls.title, + ) + return cls(max_bars=None, max_jobs_per_bar=None) + return cls() diff --git a/src/dvsim/instrumentation/report/registry.py b/src/dvsim/instrumentation/report/registry.py index bcafbf4f..d02a8d0f 100644 --- a/src/dvsim/instrumentation/report/registry.py +++ b/src/dvsim/instrumentation/report/registry.py @@ -9,8 +9,10 @@ from dvsim.instrumentation.report.base import InstrumentationVisualizer, RenderProfile from dvsim.instrumentation.report.breakdown import BlockVariantBreakdown, ToolBreakdown from dvsim.instrumentation.report.longest import ( + LongestByBlockChart, LongestByStatusChart, LongestByToolChart, + LongestTestsByBlockChart, LongestTestsByToolChart, ) from dvsim.instrumentation.report.timelines import ParallelismChart, TimelineBarChart @@ -57,6 +59,8 @@ def create(cls, profile: RenderProfile | None = None) -> list[InstrumentationVis # Register implemented / built-in instrumentation report visualizations ReportVisualizationRegistry.register(LongestByStatusChart) +ReportVisualizationRegistry.register(LongestByBlockChart) +ReportVisualizationRegistry.register(LongestTestsByBlockChart) ReportVisualizationRegistry.register(BlockVariantBreakdown) ReportVisualizationRegistry.register(LongestByToolChart) ReportVisualizationRegistry.register(LongestTestsByToolChart)