Deploying a profiling dashboard#
Install for this page:
pip install scope-profilerto write the data;npm install @scope-profiler/plotlyplus a Plotly bundle to render it.
Plotly figures for the web covers the figure builders one at a time. This page is the whole loop: one command that writes everything a page needs, and the browser-side glue that keeps a page of figures in step with a theme toggle and a filter box.
Nothing here is a framework. The output is static JSON and the input is
a <div>, so the same recipe works under Astro, Eleventy, Next, or a
hand-written index.html served from a bucket. See Try it: drag and drop a profile for
the smallest possible version: a drag-and-drop page with no build step
at all.
One command for a run’s data#
export plot-data writes the plot JSON. --with adds the three files a
page usually embeds next to the charts, so a site generator makes one
pass over the profiles instead of three:
scope-profiler export plot-data run*.h5 \
-o site/public/figures \
--format json \
--plots gantt durations timeseries flame_graph speedup \
--with
That writes, into site/public/figures/:
File |
What renders it |
|---|---|
|
|
|
the summary and comparison builders, and any HTML table |
|
any pstats viewer ( |
|
speedscope, embedded in an iframe |
|
inlined into the page, so its per-frame tooltips work |
Name the companions to pick a subset - --with prof speedscope. A bare
--with means “everything this install can produce”: the flame graph
needs the pproc extra, and without it that one export is reported and
skipped rather than failing the run. Naming --with flamegraph outright
still fails, because then you asked for that file specifically.
A page of figures#
Every figure on a real page shares two problems. Plotly bakes colours
into the layout, so a theme toggle means rebuilding every figure - which
means knowing which containers hold one and what each was built from.
And a filter box needs a syntax, which the builders deliberately do not
have: filterRegion takes a predicate, because a library should not
decide how a user spells a filter.
@scope-profiler/plotly/dashboard is a second entry point holding
exactly that glue. It is separate because it reaches for document and
window; importing @scope-profiler/plotly itself never does, and
stays usable server-side.
import Plotly from "plotly.js-dist-min";
import { createFigureRegistry } from "@scope-profiler/plotly/dashboard";
const figures = createFigureRegistry(Plotly);
// Rebuild everything when the page's theme toggle fires. The registry reads
// `data-theme` off the document element by default.
figures.watch();
const payload = await fetch("/figures/durations_data.json").then((r) => r.json());
await figures.render(document.querySelector("#durations"), payload, {
metric: "total",
regionFilter: filterBox.value,
});
render dispatches on the document’s own plot field, so one call
handles any payload. Call it again on the same container - a new metric,
an edited filter – and the figure is redrawn through Plotly’s react,
so the reader keeps the zoom and pan they had.
For a figure no plot kind names, pass the builder:
import { buildComparisonFigure } from "@scope-profiler/plotly";
await figures.render(element, statistics, {
build: buildComparisonFigure,
files: ["baseline", "candidate"],
});
figures.forget(element) stops tracking one container and purges its
plot; figures.clear() does the same for all of them. Both matter in a
single-page app, where a route change leaves Plotly’s handlers attached
to nodes the page has finished with.
The filter syntax#
A filter is a comma-separated list of terms, each matched
case-insensitively as a substring of the region name. A leading ^
anchors a term to the start, which is how a dashboard separates a group
of regions from the regions that merely mention it:
Filter |
Keeps |
|---|---|
|
every region whose name contains “solve” |
|
both groups |
|
|
(empty) |
every region - no predicate is built at all |
The same three functions drive a chart and an HTML table, so a page can filter both from one box:
import {
matchesRegionFilter,
parseRegionFilter,
regionFilter,
} from "@scope-profiler/plotly/dashboard";
// For a figure, via the registry's `regionFilter` option, or by hand:
buildDurationsFigure(payload, { filterRegion: regionFilter(box.value) });
// For a table of the same regions:
const terms = parseRegionFilter(box.value);
for (const row of tbody.querySelectorAll("tr[data-region]"))
row.hidden = terms.length && !matchesRegionFilter(row.dataset.region, terms);
regionFilter returns undefined for an empty box rather than a
predicate that accepts everything, which is both faster and what the
builders document.
What stays in your site#
The registry and the filter are the parts every deployment needs.
Everything that makes a dashboard yours stays in it: how runs are
grouped into cases, what a run’s metadata means, which regions the
filter boxes start out prefilled with, and the pages themselves.
scope-profiler has no opinion about any of that, and a page is a
better place to decide it than a profiler is.