diff --git a/devops/scripts/benchmarks/benches/base.py b/devops/scripts/benchmarks/benches/base.py
index cee03c6348985..d932e0f7390fe 100644
--- a/devops/scripts/benchmarks/benches/base.py
+++ b/devops/scripts/benchmarks/benches/base.py
@@ -7,12 +7,22 @@
import shutil
import subprocess
from pathlib import Path
+from enum import Enum
from utils.result import BenchmarkMetadata, BenchmarkTag, Result
from options import options
from utils.utils import download, run
from abc import ABC, abstractmethod
from utils.unitrace import get_unitrace
from utils.logger import log
+from utils.flamegraph import get_flamegraph
+
+
+class TracingType(Enum):
+ """Enumeration of available tracing types."""
+
+ UNITRACE = "unitrace"
+ FLAMEGRAPH = "flamegraph"
+
benchmark_tags = [
BenchmarkTag("SYCL", "Benchmark uses SYCL runtime"),
@@ -62,9 +72,10 @@ def enabled(self) -> bool:
By default, it returns True, but can be overridden to disable a benchmark."""
return True
- def traceable(self) -> bool:
- """Returns whether this benchmark should be traced by Unitrace.
- By default, it returns True, but can be overridden to disable tracing for a benchmark.
+ def traceable(self, tracing_type: TracingType) -> bool:
+ """Returns whether this benchmark should be traced by the specified tracing method.
+ By default, it returns True for all tracing types, but can be overridden
+ to disable specific tracing methods for a benchmark.
"""
return True
@@ -77,12 +88,15 @@ def teardown(self):
pass
@abstractmethod
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
"""Execute the benchmark with the given environment variables.
Args:
env_vars: Environment variables to use when running the benchmark.
run_unitrace: Whether to run benchmark under Unitrace.
+ run_flamegraph: Whether to run benchmark under FlameGraph.
Returns:
A list of Result objects with the benchmark results.
@@ -113,6 +127,8 @@ def run_bench(
use_stdout=True,
run_unitrace=False,
extra_unitrace_opt=None,
+ run_flamegraph=False,
+ extra_perf_opt=None, # VERIFY
):
env_vars = env_vars.copy()
if options.ur is not None:
@@ -125,7 +141,7 @@ def run_bench(
ld_libraries = options.extra_ld_libraries.copy()
ld_libraries.extend(ld_library)
- if self.traceable() and run_unitrace:
+ if self.traceable(TracingType.UNITRACE) and run_unitrace:
if extra_unitrace_opt is None:
extra_unitrace_opt = []
unitrace_output, command = get_unitrace().setup(
@@ -147,9 +163,41 @@ def run_bench(
get_unitrace().cleanup(options.benchmark_cwd, unitrace_output)
raise
- if self.traceable() and run_unitrace:
+ if self.traceable(TracingType.UNITRACE) and run_unitrace:
get_unitrace().handle_output(unitrace_output)
+ # flamegraph run
+
+ ld_libraries = options.extra_ld_libraries.copy()
+ ld_libraries.extend(ld_library)
+
+ perf_data_file = None
+ if self.traceable(TracingType.FLAMEGRAPH) and run_flamegraph:
+ if extra_perf_opt is None:
+ extra_perf_opt = []
+ perf_data_file, command = get_flamegraph().setup(
+ self.name(), command, extra_perf_opt
+ )
+ log.debug(f"FlameGraph perf data: {perf_data_file}")
+ log.debug(f"FlameGraph command: {' '.join(command)}")
+
+ try:
+ result = run(
+ command=command,
+ env_vars=env_vars,
+ add_sycl=add_sycl,
+ cwd=options.benchmark_cwd,
+ ld_library=ld_libraries,
+ )
+ except subprocess.CalledProcessError:
+ if run_flamegraph and perf_data_file:
+ get_flamegraph().cleanup(options.benchmark_cwd, perf_data_file)
+ raise
+
+ if self.traceable(TracingType.FLAMEGRAPH) and run_flamegraph and perf_data_file:
+ svg_file = get_flamegraph().handle_output(self.name(), perf_data_file)
+ log.info(f"FlameGraph generated: {svg_file}")
+
if use_stdout:
return result.stdout.decode()
else:
diff --git a/devops/scripts/benchmarks/benches/benchdnn.py b/devops/scripts/benchmarks/benches/benchdnn.py
index 6c00ed9771135..6d202ed3a8e4b 100644
--- a/devops/scripts/benchmarks/benches/benchdnn.py
+++ b/devops/scripts/benchmarks/benches/benchdnn.py
@@ -132,7 +132,9 @@ def setup(self):
if not self.bench_bin.exists():
raise FileNotFoundError(f"Benchmark binary not found: {self.bench_bin}")
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_flamegraph: bool = False, run_unitrace: bool = False
+ ) -> list[Result]:
command = [
str(self.bench_bin),
*self.bench_args.split(),
@@ -153,6 +155,7 @@ def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
use_stdout=True,
run_unitrace=run_unitrace,
extra_unitrace_opt=["--chrome-dnn-logging"],
+ run_flamegraph=run_flamegraph,
)
result_value = self._extract_time(output)
diff --git a/devops/scripts/benchmarks/benches/benchdnn_list.py b/devops/scripts/benchmarks/benches/benchdnn_list.py
index 53721ec1fa178..18d03fac04340 100644
--- a/devops/scripts/benchmarks/benches/benchdnn_list.py
+++ b/devops/scripts/benchmarks/benches/benchdnn_list.py
@@ -14,29 +14,29 @@
# the final choice of benchmarks to run, used in CI and other environments
benches_final_set = [
- [
- "sum",
- "f16-1",
- "--sdt=f16:f16:f16 --stag=abx:abx:abx --scales=1.25:3:0.5 16x2x6x4x3",
- False, # Do not run graph for this benchmark
- ],
- [
- "sum",
- "f16-2",
- "--reset --ddt=f16 \
- --sdt=f16:f16:f16:f16:f16:f16:f16:f16:f16:f16 \
- --stag=abx:aBx16b:ABx16a16b:ABcd16b16a:BAcd16a16b:BAcd16b16a:aBCd16b16c:aBCd16c16b:aCBd16b16c:aCBd16c16b \
- --dtag=abx,aBx16b,ABx16a16b,ABcd16b16a,BAcd16a16b,BAcd16b16a,aBCd16b16c,aBCd16c16b,aCBd16b16c,aCBd16c16b \
- --scales=1.25:3:0.5:2:0.5:2:0.5:2:0.5:2 \
- 16x32x48x5",
- False, # Do not run graph for this benchmark
- ],
- [
- "sum",
- "f32-1",
- "--sdt=bf16:bf16:bf16 --stag=abx:abx:abx --scales=0.5:2:0.5 16x2x6x4x3",
- False, # Do not run graph for this benchmark
- ],
+ # [
+ # "sum",
+ # "f16-1",
+ # "--sdt=f16:f16:f16 --stag=abx:abx:abx --scales=1.25:3:0.5 16x2x6x4x3",
+ # False, # Do not run graph for this benchmark
+ # ],
+ # [
+ # "sum",
+ # "f16-2",
+ # "--reset --ddt=f16 \
+ # --sdt=f16:f16:f16:f16:f16:f16:f16:f16:f16:f16 \
+ # --stag=abx:aBx16b:ABx16a16b:ABcd16b16a:BAcd16a16b:BAcd16b16a:aBCd16b16c:aBCd16c16b:aCBd16b16c:aCBd16c16b \
+ # --dtag=abx,aBx16b,ABx16a16b,ABcd16b16a,BAcd16a16b,BAcd16b16a,aBCd16b16c,aBCd16c16b,aCBd16b16c,aCBd16c16b \
+ # --scales=1.25:3:0.5:2:0.5:2:0.5:2:0.5:2 \
+ # 16x32x48x5",
+ # False, # Do not run graph for this benchmark
+ # ],
+ # [
+ # "sum",
+ # "f32-1",
+ # "--sdt=bf16:bf16:bf16 --stag=abx:abx:abx --scales=0.5:2:0.5 16x2x6x4x3",
+ # False, # Do not run graph for this benchmark
+ # ],
[
"sum",
"f32-2",
diff --git a/devops/scripts/benchmarks/benches/compute.py b/devops/scripts/benchmarks/benches/compute.py
index 27b0f3c0afe17..63d8989fce2e6 100644
--- a/devops/scripts/benchmarks/benches/compute.py
+++ b/devops/scripts/benchmarks/benches/compute.py
@@ -339,7 +339,9 @@ def explicit_group(self):
def description(self) -> str:
return ""
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
command = [
f"{self.benchmark_bin}",
f"--test={self.test}",
@@ -351,9 +353,7 @@ def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
env_vars.update(self.extra_env_vars())
result = self.run_bench(
- command,
- env_vars,
- run_unitrace=run_unitrace,
+ command, env_vars, run_unitrace=run_unitrace, run_flamegraph=run_flamegraph
)
parsed_results = self.parse_output(result)
ret = []
diff --git a/devops/scripts/benchmarks/benches/gromacs.py b/devops/scripts/benchmarks/benches/gromacs.py
index af64c16699118..49e602b96636b 100644
--- a/devops/scripts/benchmarks/benches/gromacs.py
+++ b/devops/scripts/benchmarks/benches/gromacs.py
@@ -169,7 +169,9 @@ def setup(self):
ld_library=self.suite.oneapi.ld_libraries(),
)
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_flamegraph: bool = False, run_unitrace: bool = False
+ ) -> list[Result]:
model_dir = self.grappa_dir / self.model
env_vars.update({"SYCL_CACHE_PERSISTENT": "1"})
@@ -209,6 +211,7 @@ def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
use_stdout=False,
ld_library=self.suite.oneapi.ld_libraries(),
run_unitrace=run_unitrace,
+ run_flamegraph=run_flamegraph,
)
if not self._validate_correctness(options.benchmark_cwd + "/md.log"):
diff --git a/devops/scripts/benchmarks/benches/llamacpp.py b/devops/scripts/benchmarks/benches/llamacpp.py
index c02e3e2335414..a78d8961e5c77 100644
--- a/devops/scripts/benchmarks/benches/llamacpp.py
+++ b/devops/scripts/benchmarks/benches/llamacpp.py
@@ -115,7 +115,9 @@ def get_tags(self):
def lower_is_better(self):
return False
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
command = [
f"{self.benchmark_bin}",
"--output",
@@ -145,6 +147,7 @@ def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
env_vars,
ld_library=self.bench.oneapi.ld_libraries(),
run_unitrace=run_unitrace,
+ run_flamegraph=run_flamegraph,
)
parsed = self.parse_output(result)
results = []
diff --git a/devops/scripts/benchmarks/benches/syclbench.py b/devops/scripts/benchmarks/benches/syclbench.py
index fc76287ed214d..0769700cf8280 100644
--- a/devops/scripts/benchmarks/benches/syclbench.py
+++ b/devops/scripts/benchmarks/benches/syclbench.py
@@ -137,7 +137,9 @@ def setup(self):
self.directory, "sycl-bench-build", self.bench_name
)
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
self.outputfile = os.path.join(self.bench.directory, self.test + ".csv")
command = [
@@ -152,9 +154,7 @@ def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
# no output to stdout, all in outputfile
self.run_bench(
- command,
- env_vars,
- run_unitrace=run_unitrace,
+ command, env_vars, run_unitrace=run_unitrace, run_flamegraph=run_flamegraph
)
with open(self.outputfile, "r") as f:
diff --git a/devops/scripts/benchmarks/benches/test.py b/devops/scripts/benchmarks/benches/test.py
index 88e5dbff7fb08..9298137314234 100644
--- a/devops/scripts/benchmarks/benches/test.py
+++ b/devops/scripts/benchmarks/benches/test.py
@@ -88,7 +88,9 @@ def notes(self) -> str:
def unstable(self) -> str:
return self.unstable_text
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
random_value = self.value + random.uniform(-1 * (self.diff), self.diff)
return [
Result(
diff --git a/devops/scripts/benchmarks/benches/umf.py b/devops/scripts/benchmarks/benches/umf.py
index 961adacc10f7c..ce275f6b7e2e3 100644
--- a/devops/scripts/benchmarks/benches/umf.py
+++ b/devops/scripts/benchmarks/benches/umf.py
@@ -137,7 +137,9 @@ def get_names_of_benchmarks_to_be_run(self, command, env_vars):
return all_names
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
command = [f"{self.benchmark_bin}"]
all_names = self.get_names_of_benchmarks_to_be_run(command, env_vars)
@@ -156,6 +158,7 @@ def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
add_sycl=False,
ld_library=[self.umf_lib],
run_unitrace=run_unitrace,
+ run_flamegraph=run_flamegraph,
)
parsed = self.parse_output(result)
diff --git a/devops/scripts/benchmarks/benches/velocity.py b/devops/scripts/benchmarks/benches/velocity.py
index a16c9d1896838..911c5f66a4e0a 100644
--- a/devops/scripts/benchmarks/benches/velocity.py
+++ b/devops/scripts/benchmarks/benches/velocity.py
@@ -130,7 +130,9 @@ def description(self) -> str:
def get_tags(self):
return ["SYCL", "application"]
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
env_vars.update(self.extra_env_vars())
command = [
@@ -143,6 +145,7 @@ def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
env_vars,
ld_library=self.ld_libraries(),
run_unitrace=run_unitrace,
+ run_flamegraph=run_flamegraph,
)
return [
@@ -287,7 +290,9 @@ class QuickSilver(VelocityBase):
def __init__(self, vb: VelocityBench):
super().__init__("QuickSilver", "qs", vb, "MMS/CTT")
- def run(self, env_vars, run_unitrace: bool = False) -> list[Result]:
+ def run(
+ self, env_vars, run_unitrace: bool = False, run_flamegraph: bool = False
+ ) -> list[Result]:
# TODO: fix the crash in QuickSilver when UR_L0_USE_IMMEDIATE_COMMANDLISTS=0
if (
"UR_L0_USE_IMMEDIATE_COMMANDLISTS" in env_vars
diff --git a/devops/scripts/benchmarks/html/data.js b/devops/scripts/benchmarks/html/data.js
index eaa5dfdf8b375..f7f135803b4e2 100644
--- a/devops/scripts/benchmarks/html/data.js
+++ b/devops/scripts/benchmarks/html/data.js
@@ -8,3 +8,8 @@
benchmarkRuns = [];
defaultCompareNames = [];
+
+benchmarkMetadata = {};
+
+benchmarkTags = {};
+
diff --git a/devops/scripts/benchmarks/html/index.html b/devops/scripts/benchmarks/html/index.html
index 43c229fb67dd9..b1771d5637d78 100644
--- a/devops/scripts/benchmarks/html/index.html
+++ b/devops/scripts/benchmarks/html/index.html
@@ -57,6 +57,10 @@
Display Options
Include archived runs
+
diff --git a/devops/scripts/benchmarks/html/scripts.js b/devops/scripts/benchmarks/html/scripts.js
index 558021a13ab4a..eba01fb5e30d8 100644
--- a/devops/scripts/benchmarks/html/scripts.js
+++ b/devops/scripts/benchmarks/html/scripts.js
@@ -17,6 +17,12 @@ let annotationsOptions = new Map(); // Global options map for annotations
let archivedDataLoaded = false;
let loadedBenchmarkRuns = []; // Loaded results from the js/json files
+// Global variables loaded from data.js:
+// - benchmarkRuns: array of benchmark run data
+// - benchmarkMetadata: metadata for benchmarks and groups
+// - benchmarkTags: tag definitions
+// - flamegraphData: available flamegraphs (optional, added dynamically)
+
// DOM Elements
let runSelect, selectedRunsDiv, suiteFiltersContainer, tagFiltersContainer;
@@ -313,8 +319,12 @@ function drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayer
const containerId = `timeseries-${index}`;
const container = createChartContainer(data, containerId, 'benchmark');
document.querySelector('.timeseries .charts').appendChild(container);
- pendingCharts.set(containerId, { data, type: 'time' });
- chartObserver.observe(container);
+
+ // Only set up chart observers if not in flamegraph mode
+ if (!isFlameGraphEnabled()) {
+ pendingCharts.set(containerId, { data, type: 'time' });
+ chartObserver.observe(container);
+ }
});
// Create layer comparison charts
@@ -322,8 +332,12 @@ function drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayer
const containerId = `layer-comparison-${index}`;
const container = createChartContainer(data, containerId, 'group');
document.querySelector('.layer-comparisons .charts').appendChild(container);
- pendingCharts.set(containerId, { data, type: 'time' });
- chartObserver.observe(container);
+
+ // Only set up chart observers if not in flamegraph mode
+ if (!isFlameGraphEnabled()) {
+ pendingCharts.set(containerId, { data, type: 'time' });
+ chartObserver.observe(container);
+ }
});
// Create bar charts
@@ -331,8 +345,12 @@ function drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayer
const containerId = `barchart-${index}`;
const container = createChartContainer(data, containerId, 'group');
document.querySelector('.bar-charts .charts').appendChild(container);
- pendingCharts.set(containerId, { data, type: 'bar' });
- chartObserver.observe(container);
+
+ // Only set up chart observers if not in flamegraph mode
+ if (!isFlameGraphEnabled()) {
+ pendingCharts.set(containerId, { data, type: 'bar' });
+ chartObserver.observe(container);
+ }
});
// Apply current filters
@@ -413,15 +431,75 @@ function createChartContainer(data, canvasId, type) {
const contentSection = document.createElement('div');
contentSection.className = 'chart-content';
- // Canvas for the chart - fixed position in content flow
- const canvas = document.createElement('canvas');
- canvas.id = canvasId;
- canvas.style.width = '100%';
-
- // Set a default height - will be properly sized later in createChart
- canvas.style.height = '400px';
- canvas.style.marginBottom = '10px';
- contentSection.appendChild(canvas);
+ // Check if flamegraph mode is enabled
+ if (isFlameGraphEnabled()) {
+ // Get all flamegraph data for this benchmark from selected runs
+ const flamegraphsToShow = getFlameGraphsForBenchmark(data.label, activeRuns);
+
+ if (flamegraphsToShow.length > 0) {
+ // Create multiple iframes for each run that has flamegraph data
+ flamegraphsToShow.forEach((flamegraphInfo, index) => {
+ const iframe = document.createElement('iframe');
+ iframe.src = flamegraphInfo.path;
+
+ // Calculate dimensions that fit within the existing container constraints
+ // The container has max-width: 1100px with 24px padding on each side
+ const containerMaxWidth = 1100;
+ const containerPadding = 48; // 24px on each side
+ const availableWidth = containerMaxWidth - containerPadding;
+
+ // Set dimensions to fit within container without scrollbars
+ iframe.style.width = '100%';
+ iframe.style.maxWidth = `${availableWidth}px`;
+ iframe.style.height = '600px';
+ iframe.style.border = '1px solid #ddd';
+ iframe.style.borderRadius = '4px';
+ iframe.style.display = 'block';
+ iframe.style.margin = index === 0 ? '0 auto 10px auto' : '10px auto'; // Add spacing between multiple iframes
+ iframe.title = `${flamegraphInfo.runName} - ${data.label}`;
+
+ // Add error handling for missing flamegraph files
+ iframe.onerror = function() {
+ const errorDiv = document.createElement('div');
+ errorDiv.className = 'flamegraph-error';
+ errorDiv.textContent = `No flamegraph available for ${flamegraphInfo.runName} - ${data.label}`;
+ contentSection.replaceChild(errorDiv, iframe);
+ };
+
+ contentSection.appendChild(iframe);
+ });
+
+ // Add resize handling to maintain proper sizing for all iframes
+ const updateIframeSizes = () => {
+ const containerMaxWidth = 1100;
+ const containerPadding = 48;
+ const availableWidth = containerMaxWidth - containerPadding;
+
+ contentSection.querySelectorAll('iframe[src*="flamegraphs"]').forEach(iframe => {
+ iframe.style.maxWidth = `${availableWidth}px`;
+ });
+ };
+
+ // Update size on window resize
+ window.addEventListener('resize', updateIframeSizes);
+ } else {
+ // Show message when no flamegraph is available
+ const noFlameGraphDiv = document.createElement('div');
+ noFlameGraphDiv.className = 'flamegraph-unavailable';
+ noFlameGraphDiv.textContent = `No flamegraph data available for ${data.label}`;
+ contentSection.appendChild(noFlameGraphDiv);
+ }
+ } else {
+ // Canvas for the chart - fixed position in content flow
+ const canvas = document.createElement('canvas');
+ canvas.id = canvasId;
+ canvas.style.width = '100%';
+
+ // Set a default height - will be properly sized later in createChart
+ canvas.style.height = '400px';
+ canvas.style.marginBottom = '10px';
+ contentSection.appendChild(canvas);
+ }
container.appendChild(contentSection);
@@ -652,6 +730,12 @@ function updateURL() {
url.searchParams.set('archived', 'true');
}
+ if (!isFlameGraphEnabled()) {
+ url.searchParams.delete('flamegraph');
+ } else {
+ url.searchParams.set('flamegraph', 'true');
+ }
+
history.replaceState(null, '', url);
}
@@ -934,6 +1018,13 @@ function setupSuiteFilters() {
});
});
+ // Debug logging for suite names
+ console.log('Available suites:', Array.from(suiteNames));
+ console.log('Loaded benchmark runs:', loadedBenchmarkRuns.map(run => ({
+ name: run.name,
+ suites: [...new Set(run.results.map(r => r.suite))]
+ })));
+
suiteNames.forEach(suite => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
@@ -968,11 +1059,107 @@ function isArchivedDataEnabled() {
return archivedDataToggle.checked;
}
+function isFlameGraphEnabled() {
+ const flameGraphToggle = document.getElementById('show-flamegraph');
+ return flameGraphToggle.checked;
+}
+
+function validateFlameGraphData() {
+ return window.flamegraphData?.runs !== undefined;
+}
+
+function createFlameGraphPath(benchmarkLabel, runName, timestamp) {
+ const benchmarkDirName = benchmarkLabel;
+ const timestampPrefix = timestamp + '_';
+ return `results/flamegraphs/${encodeURIComponent(benchmarkDirName)}/${timestampPrefix}${runName}.svg`;
+}
+
+function getRunsWithFlameGraph(benchmarkLabel, activeRuns) {
+ // Inline validation for better performance
+ if (!window.flamegraphData?.runs) {
+ return [];
+ }
+
+ const runsWithFlameGraph = [];
+ activeRuns.forEach(runName => {
+ if (flamegraphData.runs[runName] &&
+ flamegraphData.runs[runName].benchmarks &&
+ flamegraphData.runs[runName].benchmarks.includes(benchmarkLabel)) {
+ runsWithFlameGraph.push({
+ name: runName,
+ timestamp: flamegraphData.runs[runName].timestamp
+ });
+ }
+ });
+
+ return runsWithFlameGraph;
+}
+
+// Removed: getFlameGraphPath() - functionality consolidated into getFlameGraphsForBenchmark()
+
+function getFlameGraphsForBenchmark(benchmarkLabel, activeRuns) {
+ const runsWithFlameGraph = getRunsWithFlameGraph(benchmarkLabel, activeRuns);
+ const flamegraphsToShow = [];
+
+ // For each run that has flamegraph data, create the path
+ runsWithFlameGraph.forEach(runInfo => {
+ const flamegraphPath = createFlameGraphPath(benchmarkLabel, runInfo.name, runInfo.timestamp);
+
+ flamegraphsToShow.push({
+ path: flamegraphPath,
+ runName: runInfo.name,
+ timestamp: runInfo.timestamp
+ });
+ });
+
+ // Sort by the order of activeRuns to maintain consistent display order
+ const runOrder = Array.from(activeRuns);
+ flamegraphsToShow.sort((a, b) => {
+ const indexA = runOrder.indexOf(a.runName);
+ const indexB = runOrder.indexOf(b.runName);
+ return indexA - indexB;
+ });
+
+ return flamegraphsToShow;
+}
+
+// Removed: getFlameGraphInfo() - unused function, functionality covered by getFlameGraphsForBenchmark()
+
+function updateFlameGraphTooltip() {
+ const flameGraphToggle = document.getElementById('show-flamegraph');
+ const label = document.querySelector('label[for="show-flamegraph"]');
+
+ if (!flameGraphToggle || !label) return;
+
+ // Check if we have flamegraph data
+ if (validateFlameGraphData()) {
+ const runsWithFlameGraphs = Object.keys(flamegraphData.runs).filter(
+ runName => flamegraphData.runs[runName].benchmarks &&
+ flamegraphData.runs[runName].benchmarks.length > 0
+ );
+
+ if (runsWithFlameGraphs.length > 0) {
+ label.title = `Show flamegraph SVG files instead of benchmark charts. Available for runs: ${runsWithFlameGraphs.join(', ')}`;
+ flameGraphToggle.disabled = false;
+ label.style.color = '';
+ } else {
+ label.title = 'No flamegraph data available - run benchmarks with --flamegraph option to enable';
+ flameGraphToggle.disabled = true;
+ label.style.color = '#999';
+ }
+ } else {
+ label.title = 'No flamegraph data available - run benchmarks with --flamegraph option to enable';
+ flameGraphToggle.disabled = true;
+ label.style.color = '#999';
+ }
+}
+
function setupToggles() {
const notesToggle = document.getElementById('show-notes');
const unstableToggle = document.getElementById('show-unstable');
const customRangeToggle = document.getElementById('custom-range');
const archivedDataToggle = document.getElementById('show-archived-data');
+ const flameGraphToggle = document.getElementById('show-flamegraph');
notesToggle.addEventListener('change', function () {
// Update all note elements visibility
@@ -995,6 +1182,17 @@ function setupToggles() {
updateCharts();
});
+ // Add event listener for flamegraph toggle
+ if (flameGraphToggle) {
+ flameGraphToggle.addEventListener('change', function() {
+ updateCharts();
+ updateURL();
+ });
+
+ // Update flamegraph toggle tooltip with run information
+ updateFlameGraphTooltip();
+ }
+
// Add event listener for archived data toggle
if (archivedDataToggle) {
archivedDataToggle.addEventListener('change', function() {
@@ -1014,6 +1212,7 @@ function setupToggles() {
const notesParam = getQueryParam('notes');
const unstableParam = getQueryParam('unstable');
const archivedParam = getQueryParam('archived');
+ const flamegraphParam = getQueryParam('flamegraph');
if (notesParam !== null) {
let showNotes = notesParam === 'true';
@@ -1030,6 +1229,10 @@ function setupToggles() {
customRangeToggle.checked = customRangesParam === 'true';
}
+ if (flameGraphToggle && flamegraphParam !== null) {
+ flameGraphToggle.checked = flamegraphParam === 'true';
+ }
+
if (archivedDataToggle && archivedParam !== null) {
archivedDataToggle.checked = archivedParam === 'true';
@@ -1112,12 +1315,68 @@ function toggleAllTags(select) {
}
function initializeCharts() {
+ console.log('initializeCharts() started');
+
// Process raw data
+ console.log('Processing timeseries data...');
timeseriesData = processTimeseriesData();
+ console.log('Timeseries data processed:', timeseriesData.length, 'items');
+
+ console.log('Processing bar charts data...');
barChartsData = processBarChartsData();
+ console.log('Bar charts data processed:', barChartsData.length, 'items');
+
+ console.log('Processing layer comparisons data...');
layerComparisonsData = processLayerComparisonsData();
+ console.log('Layer comparisons data processed:', layerComparisonsData.length, 'items');
+
allRunNames = [...new Set(loadedBenchmarkRuns.map(run => run.name))];
latestRunsLookup = createLatestRunsLookup();
+ console.log('Run names and lookup created. Runs:', allRunNames);
+
+ // Check if we have actual benchmark results vs flamegraph-only results
+ const hasActualBenchmarks = loadedBenchmarkRuns.some(run =>
+ run.results && run.results.some(result => result.suite !== 'flamegraph')
+ );
+
+ const hasFlameGraphResults = loadedBenchmarkRuns.some(run =>
+ run.results && run.results.some(result => result.suite === 'flamegraph')
+ ) || (validateFlameGraphData() && Object.keys(flamegraphData.runs).length > 0);
+
+ console.log('Benchmark analysis:', {
+ hasActualBenchmarks,
+ hasFlameGraphResults,
+ loadedBenchmarkRuns: loadedBenchmarkRuns.length
+ });
+
+ // If we only have flamegraph results (no actual benchmark data), create synthetic data
+ if (!hasActualBenchmarks && hasFlameGraphResults) {
+ console.log('Detected flamegraph-only mode - creating synthetic data for flamegraphs');
+
+ // Check if we have flamegraph data available
+ const hasFlamegraphData = validateFlameGraphData() &&
+ Object.keys(flamegraphData.runs).length > 0 &&
+ Object.values(flamegraphData.runs).some(run => run.benchmarks && run.benchmarks.length > 0);
+
+ if (hasFlamegraphData) {
+ console.log('Creating synthetic benchmark data for flamegraph display');
+ createFlameGraphOnlyData();
+
+ // Auto-enable flamegraph mode for user convenience
+ const flameGraphToggle = document.getElementById('show-flamegraph');
+ if (flameGraphToggle && !flameGraphToggle.checked) {
+ flameGraphToggle.checked = true;
+ console.log('Auto-enabled flamegraph view for flamegraph-only data');
+ }
+ } else {
+ console.log('No flamegraph data available - showing message');
+ displayNoDataMessage();
+ }
+ } else if (!hasActualBenchmarks && !hasFlameGraphResults) {
+ // No runs and no results - something went wrong
+ console.log('No benchmark data found at all');
+ displayNoDataMessage();
+ }
// Create global options map for annotations
annotationsOptions = createAnnotationsOptions();
@@ -1414,3 +1673,154 @@ function createPlatformDetailsHTML(platform) {
function initializePlatformTab() {
displaySelectedRunsPlatformInfo();
}
+
+// Function to create chart data for flamegraph-only mode
+function createFlameGraphOnlyData() {
+ // Check if we have flamegraphData from data.js
+ if (validateFlameGraphData()) {
+ // Collect all unique benchmarks from all runs that have flamegraphs
+ const allBenchmarks = new Set();
+ const availableRuns = Object.keys(flamegraphData.runs);
+
+ availableRuns.forEach(runName => {
+ if (flamegraphData.runs[runName].benchmarks) {
+ flamegraphData.runs[runName].benchmarks.forEach(benchmark => {
+ allBenchmarks.add(benchmark);
+ });
+ }
+ });
+
+ if (allBenchmarks.size > 0) {
+ console.log(`Using flamegraphData from data.js for runs: ${availableRuns.join(', ')}`);
+ console.log(`Available benchmarks with flamegraphs: ${Array.from(allBenchmarks).join(', ')}`);
+ createSyntheticFlameGraphData(Array.from(allBenchmarks));
+ return; // Success - we have flamegraph data
+ }
+ }
+
+ // No flamegraph data available - benchmarks were run without --flamegraph option
+ console.log('No flamegraph data found - benchmarks were likely run without --flamegraph option');
+
+ // Disable the flamegraph checkbox since no flamegraphs are available
+ const flameGraphToggle = document.getElementById('show-flamegraph');
+ if (flameGraphToggle) {
+ flameGraphToggle.disabled = true;
+ flameGraphToggle.checked = false;
+
+ // Add a visual indicator that flamegraphs are not available
+ const label = document.querySelector('label[for="show-flamegraph"]');
+ if (label) {
+ label.style.color = '#999';
+ label.title = 'No flamegraph data available - run benchmarks with --flamegraph option to enable';
+ }
+
+ console.log('Disabled flamegraph toggle - no flamegraph data available');
+ }
+
+ // Clear any flamegraph-only mode detection and proceed with normal benchmark display
+ // This handles the case where we're in flamegraph-only mode but have no actual flamegraph data
+}
+
+function displayNoFlameGraphsMessage() {
+ // Clear existing data arrays
+ timeseriesData = [];
+ barChartsData = [];
+ layerComparisonsData = [];
+
+ // Add a special suite for the message
+ suiteNames.add('Information');
+
+ // Create a special entry to show a helpful message
+ const messageData = {
+ label: 'No FlameGraphs Available',
+ display_label: 'No FlameGraphs Available',
+ suite: 'Information',
+ unit: 'message',
+ lower_is_better: false,
+ range_min: null,
+ range_max: null,
+ runs: {}
+ };
+
+ timeseriesData.push(messageData);
+ console.log('Added informational message about missing flamegraphs');
+}
+
+function displayNoDataMessage() {
+ // Clear existing data arrays
+ timeseriesData = [];
+ barChartsData = [];
+ layerComparisonsData = [];
+
+ // Add a special suite for the message
+ suiteNames.add('Information');
+
+ // Create a special entry to show a helpful message
+ const messageData = {
+ label: 'No Data Available',
+ display_label: 'No Benchmark Data Available',
+ suite: 'Information',
+ unit: 'message',
+ lower_is_better: false,
+ range_min: null,
+ range_max: null,
+ runs: {}
+ };
+
+ timeseriesData.push(messageData);
+ console.log('Added informational message about missing benchmark data');
+}
+
+function createSyntheticFlameGraphData(flamegraphLabels) {
+ // Clear existing data arrays since we're in flamegraph-only mode
+ timeseriesData = [];
+ barChartsData = [];
+ layerComparisonsData = [];
+
+ // Create synthetic benchmark results for each flamegraph
+ flamegraphLabels.forEach(label => {
+ // Try to determine suite from metadata, default to "Flamegraphs"
+ const metadata = metadataForLabel(label, 'benchmark');
+ let suite = 'Flamegraphs';
+
+ // Try to match with existing metadata to get proper suite name
+ if (metadata) {
+ // Most flamegraphs are likely from Compute Benchmarks
+ suite = 'Compute Benchmarks';
+ } else {
+ // For common benchmark patterns, assume Compute Benchmarks
+ const computeBenchmarkPatterns = [
+ 'SubmitKernel', 'SubmitGraph', 'FinalizeGraph', 'SinKernelGraph',
+ 'AllocateBuffer', 'CopyBuffer', 'CopyImage', 'CreateBuffer',
+ 'CreateContext', 'CreateImage', 'CreateKernel', 'CreateProgram',
+ 'CreateQueue', 'ExecuteKernel', 'MapBuffer', 'MapImage',
+ 'ReadBuffer', 'ReadImage', 'WriteBuffer', 'WriteImage'
+ ];
+
+ if (computeBenchmarkPatterns.some(pattern => label.includes(pattern))) {
+ suite = 'Compute Benchmarks';
+ }
+ }
+
+ // Add to suite names
+ suiteNames.add(suite);
+
+ // Create a synthetic timeseries entry for this flamegraph
+ const syntheticData = {
+ label: label,
+ display_label: metadata?.display_name || label,
+ suite: suite,
+ unit: 'flamegraph',
+ lower_is_better: false,
+ range_min: null,
+ range_max: null,
+ runs: {}
+ };
+
+ // Add this to timeseriesData so it shows up in the charts
+ timeseriesData.push(syntheticData);
+ });
+
+ console.log(`Created synthetic data for ${flamegraphLabels.length} flamegraphs with suites:`, Array.from(suiteNames));
+}
+
diff --git a/devops/scripts/benchmarks/html/styles.css b/devops/scripts/benchmarks/html/styles.css
index d0f61a318fbf6..cb513a95581fa 100644
--- a/devops/scripts/benchmarks/html/styles.css
+++ b/devops/scripts/benchmarks/html/styles.css
@@ -439,4 +439,48 @@ details[open] summary::after {
margin-left: 0;
margin-top: 4px;
}
-}
\ No newline at end of file
+}
+/* FlameGraph specific styles */
+.flamegraph-error,
+.flamegraph-unavailable {
+ background-color: #f8f9fa;
+ border: 1px solid #dee2e6;
+ border-radius: 4px;
+ padding: 20px;
+ text-align: center;
+ color: #6c757d;
+ font-style: italic;
+ margin: 10px 0;
+}
+
+.flamegraph-error {
+ background-color: #fff3cd;
+ border-color: #ffeaa7;
+ color: #856404;
+}
+
+/* Updated selector to match new title format (RunName - BenchmarkName) */
+iframe[src*="flamegraphs"] {
+ transition: all 0.3s ease;
+ box-sizing: border-box;
+ max-width: 100%;
+ width: 100%;
+ border: none;
+ overflow: hidden;
+}
+
+/* Ensure flamegraph containers have proper spacing and fit within container */
+.chart-container iframe {
+ margin-bottom: 10px;
+}
+
+/* Handle multiple flamegraphs displayed vertically */
+.chart-content iframe[src*="flamegraphs"]:not(:last-child) {
+ margin-bottom: 15px;
+ border-bottom: 2px solid #e9ecef;
+}
+
+/* Add subtle visual separation between multiple flamegraphs */
+.chart-content iframe[src*="flamegraphs"]:not(:first-child) {
+ margin-top: 15px;
+}
diff --git a/devops/scripts/benchmarks/main.py b/devops/scripts/benchmarks/main.py
index 2e92c9283063c..76ef140740b9b 100755
--- a/devops/scripts/benchmarks/main.py
+++ b/devops/scripts/benchmarks/main.py
@@ -18,6 +18,7 @@
from benches.umf import *
from benches.test import TestSuite
from benches.benchdnn import OneDnnBench
+from benches.base import TracingType
from options import Compare, options
from output_markdown import generate_markdown
from output_html import generate_html
@@ -28,6 +29,7 @@
from utils.detect_versions import DetectVersion
from utils.logger import log
from utils.unitrace import get_unitrace
+from utils.flamegraph import get_flamegraph
from presets import enabled_suites, presets
# Update this if you are changing the layout of the results files
@@ -41,11 +43,14 @@ def run_iterations(
results: dict[str, list[Result]],
failures: dict[str, str],
run_unitrace: bool = False,
+ run_flamegraph: bool = False,
):
for iter in range(iters):
log.info(f"running {benchmark.name()}, iteration {iter}... ")
try:
- bench_results = benchmark.run(env_vars, run_unitrace=run_unitrace)
+ bench_results = benchmark.run(
+ env_vars, run_unitrace=run_unitrace, run_flamegraph=run_flamegraph
+ )
if bench_results is None:
if options.exit_on_failure:
raise RuntimeError(f"Benchmark produced no results!")
@@ -179,6 +184,11 @@ def main(directory, additional_env_vars, compare_names, filter):
"Unitrace requires a save name to be specified via --save option."
)
+ if options.flamegraph and options.save_name is None:
+ raise ValueError(
+ "FlameGraph requires a save name to be specified via --save option."
+ )
+
if options.build_compute_runtime:
log.info(f"Setting up Compute Runtime {options.compute_runtime_tag}")
cr = get_compute_runtime()
@@ -261,8 +271,19 @@ def main(directory, additional_env_vars, compare_names, filter):
merged_env_vars = {**additional_env_vars}
intermediate_results: dict[str, list[Result]] = {}
processed: list[Result] = []
- # regular run of the benchmark (if no unitrace or unitrace inclusive)
- if args.unitrace != "exclusive":
+
+ # Determine if we should run regular benchmarks
+ # Run regular benchmarks if:
+ # - No tracing options specified, OR
+ # - Any tracing option is set to "inclusive"
+ should_run_regular = (
+ not options.unitrace
+ and not options.flamegraph # No tracing options
+ or args.unitrace == "inclusive" # Unitrace inclusive
+ or args.flamegraph == "inclusive" # Flamegraph inclusive
+ )
+
+ if should_run_regular:
for _ in range(options.iterations_stddev):
run_iterations(
benchmark,
@@ -271,14 +292,16 @@ def main(directory, additional_env_vars, compare_names, filter):
intermediate_results,
failures,
run_unitrace=False,
+ run_flamegraph=False,
)
valid, processed = process_results(
intermediate_results, benchmark.stddev_threshold()
)
if valid:
break
+
# single unitrace run independent of benchmark iterations (if unitrace enabled)
- if options.unitrace and benchmark.traceable():
+ if options.unitrace and benchmark.traceable(TracingType.UNITRACE):
run_iterations(
benchmark,
merged_env_vars,
@@ -286,7 +309,20 @@ def main(directory, additional_env_vars, compare_names, filter):
intermediate_results,
failures,
run_unitrace=True,
+ run_flamegraph=False,
+ )
+ # single flamegraph run independent of benchmark iterations (if flamegraph enabled)
+ if options.flamegraph and benchmark.traceable(TracingType.FLAMEGRAPH):
+ run_iterations(
+ benchmark,
+ merged_env_vars,
+ 1,
+ intermediate_results,
+ failures,
+ run_unitrace=False,
+ run_flamegraph=True,
)
+
results += processed
except Exception as e:
if options.exit_on_failure:
@@ -363,6 +399,17 @@ def main(directory, additional_env_vars, compare_names, filter):
os.path.join(os.path.dirname(__file__), "html")
)
log.info(f"Generating HTML with benchmark results in {html_path}...")
+
+ # Reload history after saving to include the current results in HTML output
+ if not options.dry_run:
+ log.debug(
+ "Reloading history for HTML generation to include current results..."
+ )
+ history.load()
+ log.debug(
+ f"Reloaded {len(history.runs)} benchmark runs for HTML generation."
+ )
+
generate_html(history, compare_names, html_path, metadata)
log.info(f"HTML with benchmark results has been generated")
@@ -559,6 +606,14 @@ def validate_and_parse_env_args(env_args):
help="Unitrace tracing for single iteration of benchmarks. Inclusive tracing is done along regular benchmarks.",
choices=["inclusive", "exclusive"],
)
+ parser.add_argument(
+ "--flamegraph",
+ nargs="?",
+ const="exclusive",
+ default=None,
+ help="FlameGraph generation for single iteration of benchmarks. Inclusive generation is done along regular benchmarks.",
+ choices=["inclusive", "exclusive"],
+ )
# Options intended for CI:
parser.add_argument(
@@ -659,7 +714,8 @@ def validate_and_parse_env_args(env_args):
options.compare = Compare(args.compare_type)
options.compare_max = args.compare_max
options.output_markdown = args.output_markdown
- options.output_html = args.output_html
+ if args.output_html is not None:
+ options.output_html = args.output_html
options.dry_run = args.dry_run
options.umf = args.umf
options.iterations_stddev = args.iterations_stddev
@@ -671,6 +727,7 @@ def validate_and_parse_env_args(env_args):
options.results_directory_override = args.results_dir
options.build_jobs = args.build_jobs
options.hip_arch = args.hip_arch
+ options.flamegraph = args.flamegraph is not None
# Initialize logger with command line arguments
log.initialize(args.verbose, args.log_level)
diff --git a/devops/scripts/benchmarks/options.py b/devops/scripts/benchmarks/options.py
index 371c9b7a70403..322a68d9e211b 100644
--- a/devops/scripts/benchmarks/options.py
+++ b/devops/scripts/benchmarks/options.py
@@ -43,6 +43,7 @@ class DetectVersionsOptions:
@dataclass
class Options:
+ TIMESTAMP_FORMAT: str = "%Y%m%d_%H%M%S"
workdir: str = None
sycl: str = None
ur: str = None
@@ -71,6 +72,8 @@ class Options:
preset: str = "Full"
build_jobs: int = len(os.sched_getaffinity(0)) # Cores available for the process.
exit_on_failure: bool = False
+ save_name: str = None
+ flamegraph: bool = False
unitrace: bool = False
TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S" # Format for timestamps in filenames and logs, including Unitrace traces.
diff --git a/devops/scripts/benchmarks/output_html.py b/devops/scripts/benchmarks/output_html.py
index 3b507f639ac75..e9c0e91f012f9 100644
--- a/devops/scripts/benchmarks/output_html.py
+++ b/devops/scripts/benchmarks/output_html.py
@@ -5,6 +5,7 @@
import json
import os
+import glob
from options import options
from utils.result import BenchmarkMetadata, BenchmarkOutput
@@ -24,24 +25,67 @@ def _write_output_to_file(
if options.output_html == "local":
data_path = os.path.join(html_path, f"{filename}.js")
+
+ # Check if the file exists and has flamegraph data that we need to preserve
+ existing_flamegraph_data = None
+ if os.path.exists(data_path):
+ try:
+ with open(data_path, "r") as f:
+ existing_content = f.read()
+ # Extract existing flamegraphData if present
+ if "flamegraphData = {" in existing_content:
+ start = existing_content.find("flamegraphData = {")
+ if start != -1:
+ # Find the end of the flamegraphData object
+ brace_count = 0
+ found_start = False
+ end = start
+ for i, char in enumerate(existing_content[start:], start):
+ if char == "{":
+ brace_count += 1
+ found_start = True
+ elif char == "}" and found_start:
+ brace_count -= 1
+ if brace_count == 0:
+ end = i + 1
+ break
+ if found_start and end > start:
+ # Extract the complete flamegraphData section including the semicolon
+ next_semicolon = existing_content.find(";", end)
+ if next_semicolon != -1:
+ existing_flamegraph_data = existing_content[
+ start : next_semicolon + 1
+ ]
+ log.debug(
+ "Preserved existing flamegraph data for HTML output"
+ )
+ except Exception as e:
+ log.debug(f"Could not parse existing flamegraph data: {e}")
+
with open(data_path, "w") as f:
# For local format, we need to write JavaScript variable assignments
f.write("benchmarkRuns = ")
json.dump(json.loads(output.to_json())["runs"], f, indent=2)
f.write(";\n\n")
- f.write(f"benchmarkMetadata = ")
- json.dump(json.loads(output.to_json())["metadata"], f, indent=2)
+ f.write("defaultCompareNames = ")
+ json.dump(output.default_compare_names, f, indent=2)
f.write(";\n\n")
- f.write(f"benchmarkTags = ")
- json.dump(json.loads(output.to_json())["tags"], f, indent=2)
+ f.write("benchmarkMetadata = ")
+ json.dump(json.loads(output.to_json())["metadata"], f, indent=2)
f.write(";\n\n")
- f.write(f"defaultCompareNames = ")
- json.dump(output.default_compare_names, f, indent=2)
+ f.write("benchmarkTags = ")
+ json.dump(json.loads(output.to_json())["tags"], f, indent=2)
f.write(";\n")
+ # Preserve and append existing flamegraph data if any
+ if existing_flamegraph_data:
+ f.write("\n")
+ f.write(existing_flamegraph_data)
+ f.write("\n")
+
if not archive:
log.info(f"See {html_path}/index.html for the results.")
else:
diff --git a/devops/scripts/benchmarks/utils/flamegraph.py b/devops/scripts/benchmarks/utils/flamegraph.py
new file mode 100644
index 0000000000000..972955b0c42a2
--- /dev/null
+++ b/devops/scripts/benchmarks/utils/flamegraph.py
@@ -0,0 +1,436 @@
+# Copyright (C) 2025 Intel Corporation
+# Part of the Unified-Runtime Project, under the Apache License v2.0 with LLVM Exceptions.
+# See LICENSE.TXT
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import os
+import shutil
+import subprocess
+import signal
+import time
+
+from options import options
+from utils.utils import run, git_clone
+from utils.logger import log
+
+from datetime import datetime, timezone
+
+
+class FlameGraph:
+ """FlameGraph wrapper for managing FlameGraph tool execution and results."""
+
+ # FlameGraph SVG width to fit within web interface container
+ # Optimized width to avoid horizontal scrollbars within 1100px container
+ FLAMEGRAPH_WIDTH = 1000
+
+ def __init__(self):
+ self.timestamp = (
+ datetime.now(tz=timezone.utc).strftime(options.TIMESTAMP_FORMAT)
+ if options.timestamp_override is None
+ else options.timestamp_override
+ )
+
+ log.info("Downloading FlameGraph...")
+ repo_dir = git_clone(
+ options.workdir,
+ "flamegraph-repo",
+ "https://github.com/brendangregg/FlameGraph.git",
+ "master",
+ )
+
+ # FlameGraph doesn't need building, just verify scripts exist and are executable
+ flamegraph_scripts = [
+ "flamegraph.pl",
+ "stackcollapse-perf.pl",
+ "stackcollapse.pl",
+ ]
+
+ for script in flamegraph_scripts:
+ script_path = os.path.join(repo_dir, script)
+ # First check if script exists
+ if not os.path.exists(script_path):
+ raise FileNotFoundError(f"FlameGraph script not found: {script_path}")
+ # Then check if it's executable
+ if not os.access(script_path, os.X_OK):
+ raise RuntimeError(f"FlameGraph script not executable: {script_path}")
+ log.debug(f"Verified {script} exists and is executable")
+
+ # Store repo_dir for later use when generating flamegraphs
+ self.repo_dir = repo_dir
+
+ log.info("FlameGraph tools ready.")
+
+ if options.results_directory_override == None:
+ self.flamegraphs_dir = os.path.join(
+ options.workdir, "results", "flamegraphs"
+ )
+ else:
+ self.flamegraphs_dir = os.path.join(
+ options.results_directory_override, "flamegraphs"
+ )
+
+ def _prune_flamegraph_dirs(self, res_dir: str, FILECNT: int = 10):
+ """Keep only the last FILECNT files in the flamegraphs directory."""
+ files = os.listdir(res_dir)
+ files.sort() # Lexicographical sort matches timestamp order
+ if len(files) > 2 * FILECNT:
+ for f in files[: len(files) - 2 * FILECNT]:
+ full_path = os.path.join(res_dir, f)
+ if os.path.isdir(full_path):
+ shutil.rmtree(full_path)
+ else:
+ os.remove(full_path)
+ log.debug(f"Removing old flamegraph file: {full_path}")
+
+ def cleanup(self, bench_cwd: str, perf_data_file: str):
+ """
+ Remove incomplete output files in case of failure.
+ """
+ flamegraph_dir = os.path.dirname(perf_data_file)
+ flamegraph_base = os.path.basename(perf_data_file)
+ for f in os.listdir(flamegraph_dir):
+ if f.startswith(flamegraph_base + "."):
+ os.remove(os.path.join(flamegraph_dir, f))
+ log.debug(f"Cleanup: Removed {f} from {flamegraph_dir}")
+
+ def setup(
+ self, bench_name: str, command: list[str], extra_perf_opt: list[str] = None
+ ):
+ """
+ Prepare perf data file name and full command for the benchmark run.
+ Returns a tuple of (perf_data_file, perf_command).
+ """
+ # Check if perf is available
+ if not shutil.which("perf"):
+ raise FileNotFoundError(
+ "perf command not found. Please install linux-tools or perf package."
+ )
+
+ os.makedirs(self.flamegraphs_dir, exist_ok=True)
+ bench_dir = os.path.join(f"{self.flamegraphs_dir}", f"{bench_name}")
+
+ os.makedirs(bench_dir, exist_ok=True)
+
+ perf_data_file = os.path.join(
+ bench_dir, f"{self.timestamp}_{options.save_name}.perf.data"
+ )
+
+ if extra_perf_opt is None:
+ extra_perf_opt = []
+
+ # Default perf record options for flamegraph generation
+ perf_command = (
+ [
+ "perf",
+ "record",
+ "-g", # Enable call-graph recording
+ "-F",
+ "99", # Sample frequency
+ "--call-graph",
+ "dwarf", # Use DWARF unwinding for better stack traces
+ "-o",
+ perf_data_file,
+ ]
+ + extra_perf_opt
+ + ["--"]
+ + command
+ )
+ log.debug(f"Perf cmd: {' '.join(perf_command)}")
+
+ return perf_data_file, perf_command
+
+ def handle_output(self, bench_name: str, perf_data_file: str):
+ """
+ Generate SVG flamegraph from perf data file.
+ Returns the path to the generated SVG file.
+ """
+ if not os.path.exists(perf_data_file) or os.path.getsize(perf_data_file) == 0:
+ raise FileNotFoundError(
+ f"Perf data file not found or empty: {perf_data_file}"
+ )
+
+ # Generate output SVG filename following same pattern as perf data
+ svg_file = perf_data_file.replace(".perf.data", ".svg")
+ folded_file = perf_data_file.replace(".perf.data", ".folded")
+
+ try:
+ # Step 1: Convert perf script to folded format
+ log.debug(f"Converting perf data to folded format: {folded_file}")
+ with open(folded_file, "w") as f_folded:
+ # Run perf script to get the stack traces
+ perf_script_proc = subprocess.Popen(
+ ["perf", "script", "-i", perf_data_file],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ text=True,
+ )
+
+ # Pipe through stackcollapse-perf.pl
+ stackcollapse_perf_path = os.path.join(
+ self.repo_dir, "stackcollapse-perf.pl"
+ )
+ stackcollapse_proc = subprocess.Popen(
+ [stackcollapse_perf_path],
+ stdin=perf_script_proc.stdout,
+ stdout=f_folded,
+ stderr=subprocess.DEVNULL,
+ text=True,
+ )
+
+ perf_script_proc.stdout.close()
+ stackcollapse_proc.wait()
+ perf_script_proc.wait()
+
+ # Step 2: Generate flamegraph SVG
+ log.debug(f"Generating flamegraph SVG: {svg_file}")
+ flamegraph_pl_path = os.path.join(self.repo_dir, "flamegraph.pl")
+ with open(folded_file, "r") as f_folded, open(svg_file, "w") as f_svg:
+ flamegraph_proc = subprocess.Popen(
+ [
+ flamegraph_pl_path,
+ "--title",
+ f"{options.save_name} - {bench_name}",
+ "--width",
+ str(
+ self.FLAMEGRAPH_WIDTH
+ ), # Fit within container without scrollbars
+ ],
+ stdin=f_folded,
+ stdout=f_svg,
+ stderr=subprocess.DEVNULL,
+ text=True,
+ )
+ flamegraph_proc.wait()
+
+ # Clean up intermediate files
+ if os.path.exists(folded_file):
+ os.remove(folded_file)
+
+ if not os.path.exists(svg_file) or os.path.getsize(svg_file) == 0:
+ raise RuntimeError(f"Failed to generate flamegraph SVG: {svg_file}")
+
+ log.debug(f"Generated flamegraph: {svg_file}")
+
+ # Create symlink immediately after SVG generation
+ self._create_immediate_symlink(svg_file)
+
+ # Prune old flamegraph directories
+ self._prune_flamegraph_dirs(os.path.dirname(perf_data_file))
+
+ return svg_file
+
+ except Exception as e:
+ # Clean up on failure
+ for temp_file in [folded_file, svg_file]:
+ if os.path.exists(temp_file):
+ os.remove(temp_file)
+ raise RuntimeError(f"Failed to generate flamegraph for {bench_name}: {e}")
+
+ def _create_immediate_symlink(self, svg_file: str):
+ """
+ Create a symbolic link for the SVG file immediately after generation.
+ This ensures the web interface can access the file right away.
+ """
+ try:
+ # Check if workdir is available
+ if not options.workdir:
+ log.error("No workdir available for immediate symlink creation")
+ return
+
+ # Use the default HTML path relative to the script location
+ script_dir = os.path.dirname(os.path.dirname(__file__)) # Go up from utils/
+ html_path = os.path.join(script_dir, "html")
+
+ if not os.path.exists(html_path):
+ log.error(f"HTML directory not found: {html_path}")
+ return
+
+ # Calculate the relative path of the SVG file from the flamegraphs directory
+ if not svg_file.startswith(self.flamegraphs_dir):
+ log.error(f"SVG file not in expected flamegraphs directory: {svg_file}")
+ return
+
+ rel_path = os.path.relpath(svg_file, self.flamegraphs_dir)
+ target_dir = os.path.join(html_path, "results", "flamegraphs")
+ target_file = os.path.join(target_dir, rel_path)
+
+ # Create target directory structure
+ os.makedirs(os.path.dirname(target_file), exist_ok=True)
+
+ # Remove existing symlink if it exists
+ if os.path.islink(target_file):
+ os.unlink(target_file)
+ elif os.path.exists(target_file):
+ os.remove(target_file)
+
+ # Create the symlink
+ os.symlink(svg_file, target_file)
+ log.debug(f"Created immediate symlink: {target_file} -> {svg_file}")
+
+ # Update the flamegraph manifest for the web interface
+ self._update_flamegraph_manifest(html_path, rel_path, options.save_name)
+
+ except Exception as e:
+ log.debug(f"Failed to create immediate symlink for {svg_file}: {e}")
+
+ def _update_flamegraph_manifest(
+ self, html_path: str, svg_rel_path: str, run_name: str
+ ):
+ """
+ Update the data.js file with flamegraph information by dynamically adding the flamegraphData variable.
+ This works with a clean data.js from the repo and adds the necessary structure during execution.
+ """
+ try:
+ import re
+ from datetime import datetime
+
+ # Extract benchmark name from the relative path
+ # Format: benchmark_name/timestamp_runname.svg
+ path_parts = svg_rel_path.split("/")
+ if len(path_parts) >= 1:
+ benchmark_name = path_parts[0]
+
+ data_js_file = os.path.join(html_path, "data.js")
+
+ # Read the current data.js file
+ if not os.path.exists(data_js_file):
+ log.error(
+ f"data.js not found at {data_js_file}, cannot update flamegraph manifest"
+ )
+ return
+
+ with open(data_js_file, "r") as f:
+ content = f.read()
+
+ # Check if flamegraphData already exists
+ if "flamegraphData" not in content:
+ # Add flamegraphData object at the end of the file
+ flamegraph_data = f"""
+
+flamegraphData = {{
+ runs: {{}},
+ last_updated: '{datetime.now().isoformat()}'
+}};"""
+ content = content.rstrip() + flamegraph_data
+ log.debug("Added flamegraphData object to data.js")
+
+ # Parse and update the flamegraphData runs structure
+ flamegraph_start = content.find("flamegraphData = {")
+ if flamegraph_start != -1:
+ # Find the runs object within flamegraphData
+ runs_start = content.find("runs: {", flamegraph_start)
+ if runs_start != -1:
+ # Find the matching closing brace for the runs object
+ brace_count = 0
+ runs_content_start = runs_start + 7 # After "runs: {"
+ runs_content_end = runs_content_start
+
+ for i, char in enumerate(
+ content[runs_content_start:], runs_content_start
+ ):
+ if char == "{":
+ brace_count += 1
+ elif char == "}":
+ if brace_count == 0:
+ runs_content_end = i
+ break
+ brace_count -= 1
+
+ existing_runs_str = content[
+ runs_content_start:runs_content_end
+ ].strip()
+ existing_runs = {}
+
+ # Parse existing runs if any
+ if existing_runs_str:
+ # Simple parsing of run entries like: "RunName": { benchmarks: [...], timestamp: "..." }
+ run_matches = re.findall(
+ r'"([^"]+)":\s*\{[^}]*benchmarks:\s*\[([^\]]*)\][^}]*timestamp:\s*"([^"]*)"[^}]*\}',
+ existing_runs_str,
+ )
+
+ for run_match in run_matches:
+ existing_run_name = run_match[0]
+ existing_benchmarks_str = run_match[1]
+ existing_timestamp = run_match[2]
+
+ # Parse benchmarks array
+ existing_benchmarks = []
+ if existing_benchmarks_str.strip():
+ benchmark_matches = re.findall(
+ r'"([^"]*)"', existing_benchmarks_str
+ )
+ existing_benchmarks = benchmark_matches
+
+ existing_runs[existing_run_name] = {
+ "benchmarks": existing_benchmarks,
+ "timestamp": existing_timestamp,
+ }
+
+ # Add or update this run's benchmark
+ if run_name not in existing_runs:
+ existing_runs[run_name] = {
+ "benchmarks": [],
+ "timestamp": self.timestamp,
+ }
+ else:
+ # Update timestamp to latest for this run (in case of multiple benchmarks in same run)
+ if self.timestamp > existing_runs[run_name]["timestamp"]:
+ existing_runs[run_name]["timestamp"] = self.timestamp
+
+ # Add benchmark if not already present
+ if benchmark_name not in existing_runs[run_name]["benchmarks"]:
+ existing_runs[run_name]["benchmarks"].append(benchmark_name)
+ existing_runs[run_name]["benchmarks"].sort() # Keep sorted
+ log.debug(
+ f"Added {benchmark_name} to flamegraphData for run {run_name}"
+ )
+
+ # Create the new runs object string
+ runs_entries = []
+ for rn, data in existing_runs.items():
+ benchmarks_array = (
+ "["
+ + ", ".join(f'"{b}"' for b in data["benchmarks"])
+ + "]"
+ )
+ runs_entries.append(
+ f' "{rn}": {{\n benchmarks: {benchmarks_array},\n timestamp: "{data["timestamp"]}"\n }}'
+ )
+
+ runs_object = "{\n" + ",\n".join(runs_entries) + "\n }"
+
+ # Replace the runs object by reconstructing the content
+ before_runs = content[: runs_start + 7] # Up to "runs: {"
+ after_runs_brace = content[
+ runs_content_end:
+ ] # From the closing } onwards
+ content = (
+ before_runs + runs_object[1:-1] + after_runs_brace
+ ) # Remove outer braces from runs_object
+
+ # Update the last_updated timestamp
+ timestamp_pattern = r'(last_updated:\s*)["\'][^"\']*["\']'
+ content = re.sub(
+ timestamp_pattern,
+ rf'\g<1>"{datetime.now().isoformat()}"',
+ content,
+ )
+
+ # Write the updated content back to data.js
+ with open(data_js_file, "w") as f:
+ f.write(content)
+
+ log.debug(
+ f"Updated data.js with flamegraph data for {benchmark_name} in run {run_name}"
+ )
+
+ except Exception as e:
+ log.debug(f"Failed to update data.js with flamegraph data: {e}")
+
+
+# Singleton pattern to ensure only one instance of FlameGraph is created
+def get_flamegraph() -> FlameGraph:
+ if not hasattr(get_flamegraph, "_instance"):
+ get_flamegraph._instance = FlameGraph()
+ return get_flamegraph._instance
diff --git a/devops/scripts/benchmarks/utils/utils.py b/devops/scripts/benchmarks/utils/utils.py
index fa355f2762673..650af53cf5145 100644
--- a/devops/scripts/benchmarks/utils/utils.py
+++ b/devops/scripts/benchmarks/utils/utils.py
@@ -77,8 +77,10 @@ def run(
return result
except subprocess.CalledProcessError as e:
- log.error(e.stdout.decode())
- log.error(e.stderr.decode())
+ if e.stdout and e.stdout.decode().strip():
+ log.error(e.stdout.decode())
+ if e.stderr and e.stderr.decode().strip():
+ log.error(e.stderr.decode())
raise