From e0daae03fec878de9e6a5983eb1931f30c3fef01 Mon Sep 17 00:00:00 2001 From: David Butenhof Date: Mon, 18 Nov 2024 09:35:36 -0500 Subject: [PATCH] Improve X axis "delta time" labeling The Plotly graphing package doesn't directly support a "delta time" type, and in the comparison view we want to use delta time to compare two runs that will generally have different absolute timestamps. (It turns out that the native PatternFly graphing package, Victory, has the same limitation.) Initially, this just reported numeric delta seconds, but that's unnatural for a reader. This PR adds support for a `absolute_relative` option which reports the delta times as small absolute timestamps, like `1970-01-01 00:01:00` for 60 seconds, formatting ticks using `"%H:%M:%S"` ("00:01:00") for readability. I also made the X axis title appear, which necessitated some refactoring of the layout to avoid overlaying the legend on the axis label; and in the process I moved the "presentation specific" width parameter into the UI and the others into the API so they don't have to be duplicated in the two action calls. --- backend/app/services/crucible_svc.py | 77 ++++++++++++++++--- frontend/src/actions/ilabActions.js | 20 +---- .../templates/ILab/IlabCompareComponent.jsx | 2 +- .../templates/ILab/IlabExpandedRow.jsx | 2 +- 4 files changed, 70 insertions(+), 31 deletions(-) diff --git a/backend/app/services/crucible_svc.py b/backend/app/services/crucible_svc.py index cf9e799..aad8b29 100644 --- a/backend/app/services/crucible_svc.py +++ b/backend/app/services/crucible_svc.py @@ -16,7 +16,7 @@ from datetime import datetime, timezone from typing import Any, Iterator, Optional, Tuple, Union -from elasticsearch import AsyncElasticsearch, NotFoundError +from elasticsearch import AsyncElasticsearch from fastapi import HTTPException, status from pydantic import BaseModel @@ -67,16 +67,24 @@ class GraphList(BaseModel): Normally the X axis will be the actual sample timestamp values; if you specify relative=True, the X axis will be the duration from the first timestamp of the metric series. This allows graphs of similar runs started - at different times to be overlaid. + at different times to be overlaid. Plotly (along with other plotting + packages like PatternFly's Victory) doesn't support a "delta time" axis + unit, so also specifying absolute_relative will report relative times as + small absolute times (e.g., "1970-01-01 00:00:01" for 1 second) and a + "tick format" of "%H:%M:%S", which will look nice on the graph as long as + the total duration doesn't reach 24 hours. Without absolute_relative, the + duration is reported as numeric (floating point) seconds. Fields: name: Specify a name for the set of graphs relative: True for relative timescale + absolute_relative: True to report relative timestamps as absolute graphs: a list of Graph objects """ name: str relative: bool = False + absolute_relative: bool = False graphs: list[Metric] @@ -1869,7 +1877,42 @@ async def get_metrics_graph(self, graphdata: GraphList) -> dict[str, Any]: """ start = time.time() graphlist = [] - layout: dict[str, Any] = {"width": "1500"} + if graphdata.relative: + if graphdata.absolute_relative: + x_label = "sample runtime (HH:MM:SS)" + format = "%H:%M:%S" + else: + x_label = "sample runtime (seconds)" + format = None + else: + x_label = "sample timestamp" + format = "%Y:%M:%d %X %Z" + xaxis = { + "title": { + "text": x_label, + "font": {"color": "gray", "variant": "petite-caps", "weight": 1000}, + }, + } + if format: + xaxis["type"] = "date" + xaxis["tickformat"] = format + layout: dict[str, Any] = { + "showlegend": True, + "responsive": True, + "autosize": True, + "xaxis_title": x_label, + "yaxis_title": "Metric value", + "xaxis": xaxis, + "legend": { + "xref": "container", + "yref": "container", + "xanchor": "right", + "yanchor": "top", + "x": 0.9, + "y": 1, + "orientation": "h", + }, + } axes = {} yaxis = None cindex = 0 @@ -1891,6 +1934,9 @@ async def get_metrics_graph(self, graphdata: GraphList) -> dict[str, Any]: run_id = g.run names = g.names metric: str = g.metric + run_idx = None + if len(run_id_list) > 1: + run_idx = f"Run {run_id_list.index(run_id) + 1}" # The caller can provide a title for each graph; but, if not, we # journey down dark overgrown pathways to fabricate a default with @@ -1972,13 +2018,17 @@ async def get_metrics_graph(self, graphdata: GraphList) -> dict[str, Any]: if graphdata.relative: if not first: first = p.begin - s = (p.begin - first) / 1000.0 - e = (p.end - first) / 1000.0 + if graphdata.absolute_relative: + s = self._format_timestamp(p.begin - first) + e = self._format_timestamp(p.end - first) + else: + s = (p.begin - first) / 1000 + e = (p.end - first) / 1000 x.extend([s, e]) else: - x.extend( - [self._format_timestamp(p.begin), self._format_timestamp(p.end)] - ) + s = self._format_timestamp(p.begin) + e = self._format_timestamp(p.end) + x.extend([s, e]) y.extend([p.value, p.value]) y_max = max(y_max, p.value) @@ -1996,12 +2046,15 @@ async def get_metrics_graph(self, graphdata: GraphList) -> dict[str, Any]: "type": "scatter", "mode": "line", "marker": {"color": color}, - "labels": { - "x": "sample timestamp", - "y": "samples / second", - }, } + if run_idx: + graphitem["legendgroup"] = run_idx + graphitem["legendgrouptitle"] = { + "text": run_idx, + "font": {"variant": "small-caps", "style": "italic"}, + } + # Y-axis scaling and labeling is divided by benchmark label; # so store each we've created to reuse. (E.g., if we graph # 5 different mpstat::Busy-CPU periods, they'll share a single diff --git a/frontend/src/actions/ilabActions.js b/frontend/src/actions/ilabActions.js index d4717a8..194a19b 100644 --- a/frontend/src/actions/ilabActions.js +++ b/frontend/src/actions/ilabActions.js @@ -240,15 +240,7 @@ export const fetchGraphData = (uid) => async (dispatch, getState) => { graphs, }); if (response.status === 200) { - response.data.layout["showlegend"] = true; - response.data.layout["responsive"] = "true"; - response.data.layout["autosize"] = "true"; - response.data.layout["legend"] = { - orientation: "h", - xanchor: "left", - yanchor: "top", - y: -0.1, - }; + response.data.layout["width"] = 1500; copyData.push({ uid, data: response.data.data, @@ -334,17 +326,11 @@ export const fetchMultiGraphData = (uids) => async (dispatch, getState) => { const response = await API.post(`/api/v1/ilab/runs/multigraph`, { name: "comparison", relative: true, + absolute_relative: true, graphs, }); if (response.status === 200) { - response.data.layout["showlegend"] = true; - response.data.layout["responsive"] = "true"; - response.data.layout["autosize"] = "true"; - response.data.layout["legend"] = { - orientation: "h", - xanchor: "left", - yanchor: "top", - }; + response.data.layout["width"] = 1500; const graphData = []; graphData.push({ data: response.data.data, diff --git a/frontend/src/components/templates/ILab/IlabCompareComponent.jsx b/frontend/src/components/templates/ILab/IlabCompareComponent.jsx index 96bebd0..e81b6f6 100644 --- a/frontend/src/components/templates/ILab/IlabCompareComponent.jsx +++ b/frontend/src/components/templates/ILab/IlabCompareComponent.jsx @@ -115,7 +115,7 @@ const IlabCompareComponent = () => { type={"ilab"} /> - + diff --git a/frontend/src/components/templates/ILab/IlabExpandedRow.jsx b/frontend/src/components/templates/ILab/IlabExpandedRow.jsx index 99f095a..b16e672 100644 --- a/frontend/src/components/templates/ILab/IlabExpandedRow.jsx +++ b/frontend/src/components/templates/ILab/IlabExpandedRow.jsx @@ -69,7 +69,7 @@ const IlabRowContent = (props) => { >
Metrics:
- +