diff --git a/devops/actions/run-tests/benchmark/action.yml b/devops/actions/run-tests/benchmark/action.yml index e09582be0fde6..add36688f94b5 100644 --- a/devops/actions/run-tests/benchmark/action.yml +++ b/devops/actions/run-tests/benchmark/action.yml @@ -203,7 +203,8 @@ runs: --output-dir "./llvm-ci-perf-results/" \ --preset "$PRESET" \ --timestamp-override "$SAVE_TIMESTAMP" \ - --detect-version sycl,compute_runtime + --detect-version sycl,compute_runtime \ + --flamegraph inclusive echo "-----" python3 ./devops/scripts/benchmarks/compare.py to_hist \ diff --git a/devops/scripts/benchmarks/html/scripts.js b/devops/scripts/benchmarks/html/scripts.js index acd38b8de380c..12a75e2292deb 100644 --- a/devops/scripts/benchmarks/html/scripts.js +++ b/devops/scripts/benchmarks/html/scripts.js @@ -23,6 +23,13 @@ let loadedBenchmarkRuns = []; // Loaded results from the js/json files // - defaultCompareNames: default run names for comparison // - flamegraphData: available flamegraphs data with runs information (if available) +// Helper function to get base URL for remote or local resources +function getResourceBaseUrl() { + return typeof remoteDataUrl !== 'undefined' && remoteDataUrl !== '' + ? 'https://raw.githubusercontent.com/intel/llvm-ci-perf-results/unify-ci' + : '.'; +} + // Toggle configuration and abstraction // // HOW TO ADD A NEW TOGGLE: @@ -52,7 +59,7 @@ const toggleConfigs = { defaultValue: true, urlParam: 'notes', invertUrlParam: true, // Store false in URL when enabled (legacy behavior) - onChange: function(isEnabled) { + onChange: function (isEnabled) { document.querySelectorAll('.benchmark-note').forEach(note => { note.style.display = isEnabled ? 'block' : 'none'; }); @@ -63,7 +70,7 @@ const toggleConfigs = { defaultValue: false, urlParam: 'unstable', invertUrlParam: false, - onChange: function(isEnabled) { + onChange: function (isEnabled) { document.querySelectorAll('.benchmark-unstable').forEach(warning => { warning.style.display = isEnabled ? 'block' : 'none'; }); @@ -74,7 +81,7 @@ const toggleConfigs = { defaultValue: false, urlParam: 'customRange', invertUrlParam: false, - onChange: function(isEnabled) { + onChange: function (isEnabled) { updateCharts(); } }, @@ -82,7 +89,7 @@ const toggleConfigs = { defaultValue: false, urlParam: 'archived', invertUrlParam: false, - onChange: function(isEnabled) { + onChange: function (isEnabled) { if (isEnabled) { loadArchivedData(); } else { @@ -98,10 +105,12 @@ const toggleConfigs = { defaultValue: false, urlParam: 'flamegraph', invertUrlParam: false, - onChange: function(isEnabled) { + onChange: function (isEnabled) { // Toggle between flamegraph-only display and normal charts updateCharts(); updateFlameGraphTooltip(); + // Refresh download buttons to adapt to new mode + refreshDownloadButtons(); updateURL(); } } @@ -118,7 +127,7 @@ function setupToggle(toggleId, config) { if (!toggle) return; // Set up event listener - toggle.addEventListener('change', function() { + toggle.addEventListener('change', function () { config.onChange(toggle.checked); }); @@ -448,6 +457,19 @@ function updateCharts() { drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayerComparisonsData); } +// Function to refresh download buttons when mode changes +function refreshDownloadButtons() { + // Wait a bit for charts to be redrawn + setTimeout(() => { + document.querySelectorAll('.chart-download-button').forEach(button => { + const container = button.closest('.chart-container'); + if (container && button.updateChartButton) { + button.updateChartButton(); + } + }); + }, 100); +} + function drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayerComparisonsData) { // Clear existing charts document.querySelectorAll('.charts').forEach(container => container.innerHTML = ''); @@ -462,7 +484,7 @@ function drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayer const containerId = `timeseries-${index}`; const container = createChartContainer(data, containerId, 'benchmark'); document.querySelector('.timeseries .charts').appendChild(container); - + // Only set up chart observers if not in flamegraph mode if (!isFlameGraphEnabled()) { pendingCharts.set(containerId, { data, type: 'time' }); @@ -475,7 +497,7 @@ function drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayer const containerId = `layer-comparison-${index}`; const container = createChartContainer(data, containerId, 'group'); document.querySelector('.layer-comparisons .charts').appendChild(container); - + // Only set up chart observers if not in flamegraph mode if (!isFlameGraphEnabled()) { pendingCharts.set(containerId, { data, type: 'time' }); @@ -488,7 +510,7 @@ function drawCharts(filteredTimeseriesData, filteredBarChartsData, filteredLayer const containerId = `barchart-${index}`; const container = createChartContainer(data, containerId, 'group'); document.querySelector('.bar-charts .charts').appendChild(container); - + // Only set up chart observers if not in flamegraph mode if (!isFlameGraphEnabled()) { pendingCharts.set(containerId, { data, type: 'bar' }); @@ -576,48 +598,35 @@ function createChartContainer(data, canvasId, type) { 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 + // Add a class to reduce padding for flamegraph containers + container.classList.add('flamegraph-chart'); + // Create individual containers for each flamegraph to give them proper space flamegraphsToShow.forEach((flamegraphInfo, index) => { + // Create a dedicated container for this flamegraph + const flamegraphContainer = document.createElement('div'); + flamegraphContainer.className = 'flamegraph-container'; + const iframe = document.createElement('iframe'); iframe.src = flamegraphInfo.path; iframe.className = 'flamegraph-iframe'; - - // 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; - - // Only set max-width dynamically, other styles handled by CSS - iframe.style.maxWidth = `${availableWidth}px`; iframe.title = `${flamegraphInfo.runName} - ${data.label}`; - + // Add error handling for missing flamegraph files - iframe.onerror = function() { + 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); + flamegraphContainer.replaceChild(errorDiv, iframe); }; - - contentSection.appendChild(iframe); + + flamegraphContainer.appendChild(iframe); + contentSection.appendChild(flamegraphContainer); }); - - // 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); + + // No need for resize handling since CSS handles all sizing + // The flamegraphs will automatically use the full container width } else { // Show message when no flamegraph is available const noFlameGraphDiv = document.createElement('div'); @@ -635,6 +644,50 @@ function createChartContainer(data, canvasId, type) { container.appendChild(contentSection); + // Add simple flamegraph links below the chart: left label, inline orange links + (function addFlamegraphLinks() { + try { + const flamegraphs = getFlameGraphsForBenchmark(data.label, activeRuns || new Set()); + if (!flamegraphs || flamegraphs.length === 0) return; + + const outer = document.createElement('div'); + outer.className = 'chart-flamegraph-links'; + + const label = document.createElement('div'); + label.className = 'flamegraph-label'; + label.textContent = 'Flamegraph(s):'; + + const links = document.createElement('div'); + links.className = 'flamegraph-links-inline'; + + flamegraphs.forEach(fg => { + const a = document.createElement('a'); + a.className = 'flamegraph-link'; + a.href = fg.path; + a.target = '_blank'; + + // flame emoticon before run name + const icon = document.createElement('span'); + icon.className = 'flame-icon'; + icon.textContent = '🔥'; + + const text = document.createElement('span'); + text.className = 'flame-text'; + text.textContent = fg.runName ? `${fg.runName}${fg.timestamp ? ' — ' + fg.timestamp : ''}` : (fg.timestamp || 'Flamegraph'); + + a.appendChild(icon); + a.appendChild(text); + links.appendChild(a); + }); + + outer.appendChild(label); + outer.appendChild(links); + container.appendChild(outer); + } catch (e) { + console.error('Error while adding flamegraph links for', data.label, e); + } + })(); + // Create footer section for details const footerSection = document.createElement('div'); footerSection.className = 'chart-footer'; @@ -645,19 +698,106 @@ function createChartContainer(data, canvasId, type) { summary.className = 'download-summary'; summary.textContent = "Details"; - // Add subtle download button to the summary - const downloadButton = document.createElement('button'); - downloadButton.className = 'download-button'; - downloadButton.textContent = 'Download'; - downloadButton.onclick = (event) => { - event.stopPropagation(); // Prevent details toggle + // Helper: format Date to YYYYMMDD_HHMMSS (UTC) + function formatTimestampFromDate(d) { + if (!d) return null; + const date = (d instanceof Date) ? d : new Date(d); + if (isNaN(date)) return null; + const pad = (n) => n.toString().padStart(2, '0'); + const Y = date.getUTCFullYear(); + const M = pad(date.getUTCMonth() + 1); + const D = pad(date.getUTCDate()); + const h = pad(date.getUTCHours()); + const m = pad(date.getUTCMinutes()); + const s = pad(date.getUTCSeconds()); + return `${Y}${M}${D}_${h}${m}${s}`; + } + + // Base raw URL for archives (branch-based) + const RAW_BASE = getResourceBaseUrl(); + + // Helper function to show flamegraph list + function showFlameGraphList(flamegraphs, buttonElement) { + const existingList = document.querySelector('.download-list'); + if (existingList) existingList.remove(); + + const listContainer = document.createElement('div'); + listContainer.className = 'download-list'; + + // Dynamic positioning (kept in JS) + const rect = buttonElement.getBoundingClientRect(); + listContainer.style.top = `${window.scrollY + rect.bottom + 5}px`; + listContainer.style.left = `${window.scrollX + rect.left}px`; + + flamegraphs.forEach(fg => { + const link = document.createElement('a'); + link.href = fg.path; + link.textContent = fg.runName; + link.className = 'download-list-link'; + link.onclick = (e) => { + e.preventDefault(); + window.open(fg.path, '_blank'); + listContainer.remove(); + }; + listContainer.appendChild(link); + }); + + document.body.appendChild(listContainer); + + setTimeout(() => { + document.addEventListener('click', function closeHandler(event) { + if (!listContainer.contains(event.target) && !buttonElement.contains(event.target)) { + listContainer.remove(); + document.removeEventListener('click', closeHandler); + } + }); + }, 0); + } + + // Create Download Chart button (adapts to mode) + const chartButton = document.createElement('button'); + chartButton.className = 'download-button chart-download-button'; + chartButton.style.marginRight = '8px'; + + // Function to update button based on current mode + function updateChartButton() { if (isFlameGraphEnabled()) { - downloadFlameGraph(data.label, activeRuns, downloadButton); + const flamegraphs = getFlameGraphsForBenchmark(data.label, activeRuns); + if (flamegraphs.length === 0) { + chartButton.textContent = 'No Flamegraph Available'; + chartButton.disabled = true; + } else if (flamegraphs.length === 1) { + chartButton.textContent = 'Download Flamegraph'; + chartButton.disabled = false; + chartButton.onclick = (event) => { + event.stopPropagation(); + window.open(flamegraphs[0].path, '_blank'); + }; + } else { + chartButton.textContent = 'Download Flamegraphs'; + chartButton.disabled = false; + chartButton.onclick = (event) => { + event.stopPropagation(); + showFlameGraphList(flamegraphs, chartButton); + }; + } } else { - downloadChart(canvasId, data.label); + chartButton.textContent = 'Download Chart'; + chartButton.disabled = false; + chartButton.onclick = (event) => { + event.stopPropagation(); + downloadChart(canvasId, data.label); + }; } - }; - summary.appendChild(downloadButton); + } + + updateChartButton(); + + // Store the update function on the button so it can be called when mode changes + chartButton.updateChartButton = updateChartButton; + + // Append the chart download button to the summary + summary.appendChild(chartButton); details.appendChild(summary); // Create and append extra info @@ -1245,20 +1385,42 @@ function validateFlameGraphData() { return window.flamegraphData?.runs !== undefined; } +function sanitizeFilename(name) { + /** + * Sanitize a string to be safe for use as a filename or directory name. + * Replace invalid characters (including space) with underscores so paths are shell-safe. + * + * Invalid characters: " : < > | * ? \r \n + */ + const invalidChars = /[":;<>|*?\r\n ]/g; // Added space to align with Python implementation + return name.replace(invalidChars, '_'); +} + function createFlameGraphPath(benchmarkLabel, runName, timestamp) { const suiteName = window.flamegraphData?.runs?.[runName]?.suites?.[benchmarkLabel]; if (!suiteName) { console.error(`Could not find suite for benchmark '${benchmarkLabel}' in run '${runName}'`); - // Fallback to old path for safety, though it's likely to fail. - const benchmarkDirName = benchmarkLabel; + // Fallback: sanitize benchmark name for directory structure + const sanitizedBenchmarkName = sanitizeFilename(benchmarkLabel); const timestampPrefix = timestamp + '_'; - return `results/flamegraphs/${benchmarkDirName}/${timestampPrefix}${runName}.svg`; + const relativePath = `results/flamegraphs/${sanitizedBenchmarkName}/${timestampPrefix}${runName}.svg`; + + // For local mode, use relative path; for remote mode, use full URL + const baseUrl = getResourceBaseUrl(); + return baseUrl === '.' ? relativePath : `${baseUrl}/${relativePath}`; } - const benchmarkDirName = `${suiteName}__${benchmarkLabel}`; + // Apply sanitization to both suite and benchmark names to match Python implementation + const sanitizedSuiteName = sanitizeFilename(suiteName); + const sanitizedBenchmarkName = sanitizeFilename(benchmarkLabel); + const benchmarkDirName = `${sanitizedSuiteName}__${sanitizedBenchmarkName}`; const timestampPrefix = timestamp + '_'; - return `results/flamegraphs/${benchmarkDirName}/${timestampPrefix}${runName}.svg`; + const relativePath = `results/flamegraphs/${benchmarkDirName}/${timestampPrefix}${runName}.svg`; + + // For local mode, use relative path; for remote mode, use full URL + const baseUrl = getResourceBaseUrl(); + return baseUrl === '.' ? relativePath : `${baseUrl}/${relativePath}`; } function getRunsWithFlameGraph(benchmarkLabel, activeRuns) { @@ -1266,11 +1428,11 @@ function getRunsWithFlameGraph(benchmarkLabel, activeRuns) { if (!window.flamegraphData?.runs) { return []; } - + const runsWithFlameGraph = []; activeRuns.forEach(runName => { - if (window.flamegraphData.runs[runName] && - window.flamegraphData.runs[runName].suites && + if (window.flamegraphData.runs[runName] && + window.flamegraphData.runs[runName].suites && Object.keys(window.flamegraphData.runs[runName].suites).includes(benchmarkLabel)) { runsWithFlameGraph.push({ name: runName, @@ -1278,25 +1440,25 @@ function getRunsWithFlameGraph(benchmarkLabel, activeRuns) { }); } }); - + return runsWithFlameGraph; } 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) => { @@ -1304,23 +1466,23 @@ function getFlameGraphsForBenchmark(benchmarkLabel, activeRuns) { const indexB = runOrder.indexOf(b.runName); return indexA - indexB; }); - + return flamegraphsToShow; } 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(window.flamegraphData.runs).filter( - runName => window.flamegraphData.runs[runName].suites && - Object.keys(window.flamegraphData.runs[runName].suites).length > 0 + runName => window.flamegraphData.runs[runName].suites && + Object.keys(window.flamegraphData.runs[runName].suites).length > 0 ); - + if (runsWithFlameGraphs.length > 0) { label.title = `Show flamegraph SVG files instead of benchmark charts. Available for runs: ${runsWithFlameGraphs.join(', ')}`; flameGraphToggle.disabled = false; @@ -1421,37 +1583,42 @@ function toggleAllTags(select) { function initializeCharts() { console.log('initializeCharts() started'); - + console.log('loadedBenchmarkRuns:', loadedBenchmarkRuns.length, 'runs'); + console.log('First run name:', loadedBenchmarkRuns.length > 0 ? loadedBenchmarkRuns[0].name : 'no runs'); + console.log('defaultCompareNames:', defaultCompareNames); + // 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))]; - + console.log('All run names:', allRunNames); + // In flamegraph-only mode, ensure we include runs from flamegraph data if (validateFlameGraphData()) { const flamegraphRunNames = Object.keys(window.flamegraphData.runs); allRunNames = [...new Set([...allRunNames, ...flamegraphRunNames])]; + console.log('Added flamegraph runs, total run names:', allRunNames); } - + 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 => + const hasActualBenchmarks = loadedBenchmarkRuns.some(run => run.results && run.results.length > 0 && run.results.some(result => result.suite !== 'flamegraph') ); - - const hasFlameGraphResults = loadedBenchmarkRuns.some(run => + + const hasFlameGraphResults = loadedBenchmarkRuns.some(run => run.results && run.results.some(result => result.suite === 'flamegraph') ) || (validateFlameGraphData() && Object.keys(window.flamegraphData.runs).length > 0); @@ -1471,16 +1638,16 @@ function initializeCharts() { // 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(window.flamegraphData.runs).length > 0 && - Object.values(window.flamegraphData.runs).some(run => run.suites && Object.keys(run.suites).length > 0); - + const hasFlamegraphData = validateFlameGraphData() && + Object.keys(window.flamegraphData.runs).length > 0 && + Object.values(window.flamegraphData.runs).some(run => run.suites && Object.keys(run.suites).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) { @@ -1527,7 +1694,7 @@ function initializeCharts() { } else { // No runs parameter, use defaults activeRuns = new Set(defaultCompareNames || []); - + // If no default runs and we're in flamegraph-only mode, use all available runs if (activeRuns.size === 0 && !hasActualBenchmarks && hasFlameGraphResults) { activeRuns = new Set(allRunNames); @@ -1589,22 +1756,30 @@ window.toggleAllTags = toggleAllTags; // Helper function to fetch and process benchmark data function fetchAndProcessData(url, isArchived = false) { const loadingIndicator = document.getElementById('loading-indicator'); - return fetch(url) - .then(response => { - if (!response.ok) { throw new Error(`Got response status ${response.status}.`) } - return response.json(); - }) + .then(resp => { if (!resp.ok) throw new Error(`Got response status ${resp.status}.`); return resp.json(); }) .then(data => { - const newRuns = data.runs || data; - + const runsArray = Array.isArray(data.benchmarkRuns) ? data.benchmarkRuns : data.runs; + if (!runsArray || !Array.isArray(runsArray)) { + throw new Error('Invalid data format: expected benchmarkRuns or runs array'); + } if (isArchived) { - // Merge with existing data for archived data - loadedBenchmarkRuns = loadedBenchmarkRuns.concat(newRuns); + loadedBenchmarkRuns = loadedBenchmarkRuns.concat(runsArray); archivedDataLoaded = true; } else { - // Replace existing data for current data - loadedBenchmarkRuns = newRuns; + loadedBenchmarkRuns = runsArray; + window.benchmarkMetadata = data.benchmarkMetadata || data.metadata || {}; + window.benchmarkTags = data.benchmarkTags || data.tags || {}; + window.flamegraphData = (data.flamegraphData && data.flamegraphData.runs) ? data.flamegraphData : { runs: {} }; + if (Array.isArray(data.defaultCompareNames)) { + defaultCompareNames = data.defaultCompareNames; + } + console.log('Remote data loaded (normalized):', { + runs: runsArray.length, + metadata: Object.keys(window.benchmarkMetadata).length, + tags: Object.keys(window.benchmarkTags).length, + flamegraphs: Object.keys(window.flamegraphData.runs).length + }); } // The following variables have same values regardless of whether @@ -1614,44 +1789,51 @@ function fetchAndProcessData(url, isArchived = false) { initializeCharts(); }) - .catch(error => { - console.error(`Error fetching ${isArchived ? 'archived' : 'remote'} data:`, error); + .catch(err => { + console.error(`Error fetching ${isArchived ? 'archived' : 'remote'} data:`, err); loadingIndicator.textContent = 'Fetching remote data failed.'; }) - .finally(() => { - loadingIndicator.classList.add('hidden'); - }); + .finally(() => loadingIndicator.classList.add('hidden')); } // Load data based on configuration function loadData() { + console.log('=== loadData() called ==='); const loadingIndicator = document.getElementById('loading-indicator'); loadingIndicator.classList.remove('hidden'); // Show loading indicator if (typeof remoteDataUrl !== 'undefined' && remoteDataUrl !== '') { + console.log('Using remote data URL:', remoteDataUrl); // Fetch data from remote URL - const url = remoteDataUrl.endsWith('/') ? remoteDataUrl + 'data.json' : remoteDataUrl + '/data.json'; - fetchAndProcessData(url); + fetchAndProcessData(remoteDataUrl); } else { - // Use local data - loadedBenchmarkRuns = benchmarkRuns; - // Assign global metadata from data.js if window.benchmarkMetadata is not set - if (!window.benchmarkMetadata) { - window.benchmarkMetadata = (typeof benchmarkMetadata !== 'undefined') ? benchmarkMetadata : {}; - } - // Assign global tags from data.js if window.benchmarkTags is not set - if (!window.benchmarkTags) { - window.benchmarkTags = (typeof benchmarkTags !== 'undefined') ? benchmarkTags : {}; - } - // Assign flamegraph data from data.js if available - if (typeof flamegraphData !== 'undefined') { - window.flamegraphData = flamegraphData; - console.log('Loaded flamegraph data from data.js with', Object.keys(flamegraphData.runs || {}).length, 'runs'); - } else { + console.log('Using local canonical data'); + if (!Array.isArray(window.benchmarkRuns)) { + console.error('benchmarkRuns missing or invalid'); + loadedBenchmarkRuns = []; + window.benchmarkMetadata = {}; + window.benchmarkTags = {}; window.flamegraphData = { runs: {} }; + } else { + loadedBenchmarkRuns = window.benchmarkRuns; + window.benchmarkMetadata = window.benchmarkMetadata || {}; + window.benchmarkTags = window.benchmarkTags || {}; + window.flamegraphData = (window.flamegraphData && window.flamegraphData.runs) ? window.flamegraphData : { runs: {} }; + console.log('Local data loaded (standalone globals):', { + runs: loadedBenchmarkRuns.length, + metadata: Object.keys(window.benchmarkMetadata).length, + tags: Object.keys(window.benchmarkTags).length, + flamegraphs: Object.keys(window.flamegraphData.runs).length + }); } initializeCharts(); - loadingIndicator.classList.add('hidden'); // Hide loading indicator + if (loadedBenchmarkRuns.length === 0) { + loadingIndicator.textContent = 'No benchmark data found.'; + loadingIndicator.setAttribute('role', 'alert'); // optional accessibility + } else { + loadingIndicator.classList.add('hidden'); // hide when data present + } + console.log('=== loadData() completed ==='); } } @@ -1748,17 +1930,17 @@ function displaySelectedRunsPlatformInfo() { selectedRunsWithPlatform.forEach(runData => { const runSection = document.createElement('div'); runSection.className = 'platform-run-section'; - const runTitle = document.createElement('h3'); + const runTitle = document.createElement('h3'); runTitle.textContent = `Run: ${runData.name}`; runTitle.className = 'platform-run-title'; runSection.appendChild(runTitle); - // Create just the platform details without the grid wrapper + // Create just the platform details without the grid wrapper const platform = runData.platform; const detailsContainer = document.createElement('div'); detailsContainer.className = 'platform-details-compact'; detailsContainer.innerHTML = createPlatformDetailsHTML(platform); runSection.appendChild(detailsContainer); - container.appendChild(runSection); + container.appendChild(runSection); }); } @@ -1792,8 +1974,8 @@ function createPlatformDetailsHTML(platform) { GPU:
${platform.gpu_info && platform.gpu_info.length > 0 - ? platform.gpu_info.map(gpu => `
• ${gpu}
`).join('') - : '
• No GPU detected
'} + ? platform.gpu_info.map(gpu => `
• ${gpu}
`).join('') + : '
• No GPU detected
'}
@@ -1823,7 +2005,7 @@ function createFlameGraphOnlyData() { // Collect all unique benchmarks from all runs that have flamegraphs const allBenchmarks = new Set(); const availableRuns = Object.keys(window.flamegraphData.runs); - + availableRuns.forEach(runName => { if (window.flamegraphData.runs[runName].suites) { Object.keys(window.flamegraphData.runs[runName].suites).forEach(benchmark => { @@ -1831,7 +2013,7 @@ function createFlameGraphOnlyData() { }); } }); - + 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(', ')}`); @@ -1839,26 +2021,26 @@ function createFlameGraphOnlyData() { 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.classList.add('disabled-text'); 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 } @@ -1868,10 +2050,10 @@ function displayNoFlameGraphsMessage() { 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', @@ -1883,7 +2065,7 @@ function displayNoFlameGraphsMessage() { range_max: null, runs: {} }; - + timeseriesData.push(messageData); console.log('Added informational message about missing flamegraphs'); } @@ -1893,10 +2075,10 @@ function displayNoDataMessage() { 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', @@ -1908,7 +2090,7 @@ function displayNoDataMessage() { range_max: null, runs: {} }; - + timeseriesData.push(messageData); console.log('Added informational message about missing benchmark data'); } @@ -1918,12 +2100,12 @@ function createSyntheticFlameGraphData(flamegraphLabels) { timeseriesData = []; barChartsData = []; layerComparisonsData = []; - + // Create synthetic benchmark results for each flamegraph flamegraphLabels.forEach(label => { // Get suite from flamegraphData - this should always be available let suite = null; - + if (window.flamegraphData?.runs) { // Check all runs for suite information for this benchmark for (const runName in window.flamegraphData.runs) { @@ -1934,16 +2116,16 @@ function createSyntheticFlameGraphData(flamegraphLabels) { } } } - + // If no suite found, this indicates a problem with the flamegraph data generation if (!suite) { console.error(`No suite information found for flamegraph: ${label}. This indicates missing suite data in flamegraphs.js`); suite = `ERROR: Missing suite for ${label}`; } - + // Add to suite names suiteNames.add(suite); - + // Create a synthetic timeseries entry for this flamegraph const syntheticData = { label: label, @@ -1955,11 +2137,11 @@ function createSyntheticFlameGraphData(flamegraphLabels) { 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 96c81223ecb40..7b68942b85bc5 100644 --- a/devops/scripts/benchmarks/html/styles.css +++ b/devops/scripts/benchmarks/html/styles.css @@ -3,8 +3,11 @@ ======================================== */ :root { /* Core Color Palette - only used colors from scripts.js */ - --color-red: rgb(255, 50, 80); - --color-orange: rgb(255, 145, 15); + /* Provide RGB component vars to allow reuse in rgba() backgrounds */ + --color-red-rgb: 255, 50, 80; + --color-orange-rgb: 255, 145, 15; + --color-red: rgb(var(--color-red-rgb)); + --color-orange: rgb(var(--color-orange-rgb)); --color-yellow: rgb(255, 220, 0); --color-green: rgb(20, 200, 50); --color-blue: rgb(0, 130, 255); @@ -24,12 +27,14 @@ /* Backgrounds - consolidated similar grays */ --bg-body: #f8f9fa; /* Replaces bg-lighter, bg-light-gray */ --bg-white: white; + --bg-hover: #f5f5f5; /* used for hover states (was fallback) */ --bg-light: #e9ecef; /* Replaces bg-disabled */ --bg-summary: #dee2e6; --bg-summary-hover: #ced4da; --bg-info: #cfe2ff; - --bg-warning: rgba(255, 145, 15, 0.1); /* Light orange background for warnings */ - --bg-danger: rgba(255, 50, 80, 0.1); /* Light red background for errors */ + --bg-warning: rgba(var(--color-orange-rgb), 0.1); /* Light orange background for warnings */ + --bg-danger: rgba(var(--color-red-rgb), 0.1); /* Light red background for errors */ + background: var(--bg-white); /* Borders - simplified */ --border-light: #ccc; @@ -57,10 +62,10 @@ h1, h2 { font-weight: 500; } .chart-container { - background: white; - border-radius: 8px; + /* Removed direct literal 'white'; using themed hover background */ + background: var(--bg-hover); padding: 24px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: var(--shadow-subtle); position: relative; display: flex; flex-direction: column; @@ -168,7 +173,7 @@ details[open] summary::after { .run-selector button { padding: 8px 16px; background: var(--color-blue); - color: white; + color: var(--bg-white); border: none; border-radius: 4px; cursor: pointer; @@ -206,6 +211,51 @@ details[open] summary::after { .download-button:hover { color: var(--color-cyan); } + +.download-button:disabled { + color: var(--text-muted); + cursor: not-allowed; +} + +.download-list { + position: absolute; + z-index: 1000; + background: var(--bg-white); + border: 1px solid var(--border-medium); + border-radius: 4px; + padding: 4px; + min-width: 200px; + max-width: 300px; + box-shadow: var(--shadow-dropdown); + font-size: 0.9rem; +} + +/* Support both legacy .download-list-link and generic anchors inside list */ +.download-list a, +.download-list-link { + display: block; + padding: 8px 12px; + text-decoration: none; + color: var(--text-dark); + border-radius: 2px; + font-size: 14px; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.download-list a:hover, +.download-list a:focus, +.download-list-link:hover, +.download-list-link:focus { + background: var(--bg-light); + outline: none; +} + +.chart-download-button { + margin-right: 8px; +} .loading-indicator { text-align: center; font-size: 18px; @@ -369,6 +419,48 @@ details[open] summary::after { cursor: help; font-size: 12px; } + +/* Flamegraph link area styles (used by scripts.js) */ +.chart-flamegraph-links { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; + /* small margin below links to separate from any gray bar or footer */ + margin-bottom: 6px; +} +.flamegraph-label { + color: var(--text-dark); + font-weight: 600; +} +.flamegraph-links-inline { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.flamegraph-link { + color: var(--text-warning); + text-decoration: none; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 6px; +} +.flamegraph-link:hover { + text-decoration: underline; +} +.flame-icon { + font-size: 16px; + line-height: 1; + display: inline-block; +} +.flame-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 220px; + display: inline-block; +} #tag-filters { display: flex; flex-wrap: wrap; @@ -395,7 +487,7 @@ details[open] summary::after { .remove-tag { background: none; border: none; - color: white; + color: var(--bg-white); margin-left: 4px; cursor: pointer; font-size: 16px; @@ -406,7 +498,7 @@ details[open] summary::after { } .platform { padding: 16px; - background: white; + background: var(--bg-white); border-radius: 8px; margin-top: 8px; } @@ -496,9 +588,10 @@ details[open] summary::after { } .flamegraph-error { - background-color: var(--bg-warning); - border-color: var(--color-orange); - color: var(--text-warning); + /* Repurposed to use dedicated danger background */ + background-color: var(--bg-danger); + border-color: var(--text-danger); + color: var(--text-danger); } /* ======================================== @@ -530,30 +623,63 @@ details[open] summary::after { border: 1px solid var(--border-medium); border-radius: 4px; display: block; - margin: 10px auto; + margin: 0 0 10px 0; transition: all 0.3s ease; box-sizing: border-box; overflow: hidden; + /* Ensure maximum width utilization */ + max-width: none; + min-width: 0; } -.flamegraph-iframe:first-child { - margin: 0 auto 10px auto; +/* Flamegraph container styles - gives each flamegraph its own space */ +.flamegraph-container { + margin-bottom: 20px; + padding: 0; + border: none; + border-radius: 0; + background-color: transparent; + width: 100%; + /* Ensure no width constraints */ + max-width: none; + min-width: 0; + box-sizing: border-box; +} + +.flamegraph-container:last-child { + margin-bottom: 0; +} + +.flamegraph-title { + font-size: 16px; + font-weight: 600; + color: var(--text-dark); + margin: 0 0 10px 0; + padding: 8px 12px; + background-color: var(--bg-light); + border-radius: 4px; + border-left: 4px solid var(--color-blue); } /* Ensure flamegraph containers have proper spacing and fit within container */ -.chart-container iframe { - margin-bottom: 10px; +.chart-container .flamegraph-container { + margin-bottom: 20px; + width: 100%; } -/* Handle multiple flamegraphs displayed vertically */ -.chart-content iframe[src*="flamegraphs"]:not(:last-child) { - margin-bottom: 15px; - border-bottom: 2px solid var(--bg-light); +/* Reduce padding for chart containers that contain flamegraphs to maximize space */ +.chart-container.flamegraph-chart { + padding: 8px; +} + +/* Ensure chart content doesn't constrain flamegraphs */ +.chart-container.flamegraph-chart .chart-content { + padding: 0; } -/* Add subtle visual separation between multiple flamegraphs */ -.chart-content iframe[src*="flamegraphs"]:not(:first-child) { - margin-top: 15px; +/* Handle multiple flamegraphs displayed vertically - now handled by container */ +.flamegraph-container:not(:last-child) { + margin-bottom: 20px; } /* Floating flamegraph download list */ @@ -562,8 +688,8 @@ details[open] summary::after { z-index: 1000; border: 1px solid var(--border-light); border-radius: 4px; - background-color: white; - box-shadow: 0 2px 5px rgba(0,0,0,0.15); + background-color: var(--bg-white); + box-shadow: var(--shadow-dropdown); padding: 5px; margin-top: 5px; } diff --git a/devops/scripts/benchmarks/output_html.py b/devops/scripts/benchmarks/output_html.py index 621ed884aed0f..cad51c94d30c8 100644 --- a/devops/scripts/benchmarks/output_html.py +++ b/devops/scripts/benchmarks/output_html.py @@ -70,53 +70,54 @@ def _write_output_to_file( """ # Define variable configuration based on whether we're archiving or not filename = "data_archive" if archive else "data" + output_data = json.loads(output.to_json()) # type: ignore + + if options.flamegraph: + flamegraph_data = _get_flamegraph_data(html_path) + if flamegraph_data and flamegraph_data.get("runs"): + output_data["flamegraphData"] = flamegraph_data + log.debug( + f"Added flamegraph data for {len(flamegraph_data['runs'])} runs to {filename}.*" + ) + + runs_list = output_data.get("runs", []) if options.output_html == "local": + # Local JS: emit standalone globals (legacy-style) without wrapper object data_path = os.path.join(html_path, f"{filename}.js") 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) # type: ignore - f.write(";\n\n") - - f.write(f"benchmarkMetadata = ") - json.dump(json.loads(output.to_json())["metadata"], f, indent=2) # type: ignore - f.write(";\n\n") - - f.write(f"benchmarkTags = ") - json.dump(json.loads(output.to_json())["tags"], f, indent=2) # type: ignore - f.write(";\n\n") - - f.write(f"defaultCompareNames = ") - json.dump(output.default_compare_names, f, indent=2) - f.write(";\n\n") - - # Add flamegraph data if it exists - if options.flamegraph: - flamegraph_data = _get_flamegraph_data(html_path) - if flamegraph_data and flamegraph_data.get("runs"): - f.write("flamegraphData = ") - json.dump(flamegraph_data, f, indent=2) - f.write(";\n\n") - log.debug( - f"Added flamegraph data for {len(flamegraph_data['runs'])} runs to data.js" - ) - - if not archive: - log.info(f"See {html_path}/index.html for the results.") + json.dump(runs_list, f, indent=2) + f.write(";\n") + if "flamegraphData" in output_data: + f.write("flamegraphData = ") + json.dump(output_data["flamegraphData"], f, indent=2) + f.write(";\n") + else: + f.write("flamegraphData = { runs: {} };\n") + f.write("benchmarkMetadata = ") + json.dump(output_data.get("metadata", {}), f, indent=2) + f.write(";\n") + f.write("benchmarkTags = ") + json.dump(output_data.get("tags", {}), f, indent=2) + f.write(";\n") + f.write("defaultCompareNames = ") + json.dump(output.default_compare_names, f) + f.write(";\n") + if not archive: + log.info(f"See {html_path}/index.html for the results.") else: - # For remote format, we write a single JSON file + # Remote JSON: emit flat schema aligning with local globals + remote_obj = { + "benchmarkRuns": runs_list, + "benchmarkMetadata": output_data.get("metadata", {}), + "benchmarkTags": output_data.get("tags", {}), + "flamegraphData": output_data.get("flamegraphData", {"runs": {}}), + "defaultCompareNames": output.default_compare_names, + } data_path = os.path.join(html_path, f"{filename}.json") - output_data = json.loads(output.to_json()) # type: ignore - if options.flamegraph: - flamegraph_data = _get_flamegraph_data(html_path) - if flamegraph_data and flamegraph_data.get("runs"): - output_data["flamegraphs"] = flamegraph_data - log.debug( - f"Added flamegraph data for {len(flamegraph_data['runs'])} runs to {filename}.json" - ) with open(data_path, "w") as f: - json.dump(output_data, f, indent=2) + json.dump(remote_obj, f, indent=2) log.info( f"Upload {data_path} to a location set in config.js remoteDataUrl argument." ) diff --git a/devops/scripts/benchmarks/utils/flamegraph.py b/devops/scripts/benchmarks/utils/flamegraph.py index ae51b0eae9d25..9e445855cc054 100644 --- a/devops/scripts/benchmarks/utils/flamegraph.py +++ b/devops/scripts/benchmarks/utils/flamegraph.py @@ -7,7 +7,12 @@ from pathlib import Path from options import options -from utils.utils import run, prune_old_files, remove_by_prefix +from utils.utils import ( + run, + prune_old_files, + remove_by_prefix, + sanitize_filename, +) from utils.logger import log from git_project import GitProject from datetime import datetime, timezone @@ -38,7 +43,7 @@ def __init__(self): if options.results_directory_override: self.flamegraphs_dir = ( - Path(options.results_directory_override) / "flamegraphs" + Path(options.results_directory_override) / "results" / "flamegraphs" ) else: self.flamegraphs_dir = Path(options.workdir) / "results" / "flamegraphs" @@ -65,7 +70,9 @@ def setup( "perf command not found. Please install linux-tools or perf package." ) - dir_name = f"{suite_name}__{bench_name}" + sanitized_suite_name = sanitize_filename(suite_name) + sanitized_bench_name = sanitize_filename(bench_name) + dir_name = f"{sanitized_suite_name}__{sanitized_bench_name}" bench_dir = self.flamegraphs_dir / dir_name bench_dir.mkdir(parents=True, exist_ok=True) @@ -104,13 +111,18 @@ def handle_output(self, bench_name: str, perf_data_file: str, suite_name: str = perf_data_path.stem.replace(".perf", "") + ".svg" ) folded_file = perf_data_path.with_suffix(".folded") - try: self._convert_perf_to_folded(perf_data_path, folded_file) self._generate_svg(folded_file, svg_file, bench_name) - log.debug(f"Generated flamegraph: {svg_file}") + log.info(f"FlameGraph SVG created: {svg_file.resolve()}") self._create_immediate_symlink(svg_file) + + # Clean up the original perf data file after successful SVG generation + if perf_data_path.exists(): + perf_data_path.unlink() + log.debug(f"Removed original perf data file: {perf_data_path}") + prune_old_files(str(perf_data_path.parent)) return str(svg_file) except Exception as e: diff --git a/devops/scripts/benchmarks/utils/unitrace.py b/devops/scripts/benchmarks/utils/unitrace.py index e37c02d8687cc..f788b11c69be3 100644 --- a/devops/scripts/benchmarks/utils/unitrace.py +++ b/devops/scripts/benchmarks/utils/unitrace.py @@ -14,6 +14,7 @@ prune_old_files, remove_by_prefix, remove_by_extension, + sanitize_filename, ) from utils.logger import log from git_project import GitProject @@ -66,7 +67,9 @@ def __init__(self): if options.results_directory_override == None: self.traces_dir = os.path.join(options.workdir, "results", "traces") else: - self.traces_dir = os.path.join(options.results_directory_override, "traces") + self.traces_dir = os.path.join( + options.results_directory_override, "results", "traces" + ) def _prune_unitrace_dirs(self, res_dir: str, FILECNT: int = 10): """Keep only the last FILECNT files in the traces directory.""" @@ -92,7 +95,8 @@ def setup( if not os.path.exists(unitrace_bin): raise FileNotFoundError(f"Unitrace binary not found: {unitrace_bin}. ") os.makedirs(self.traces_dir, exist_ok=True) - bench_dir = os.path.join(f"{self.traces_dir}", f"{bench_name}") + sanitized_bench_name = sanitize_filename(bench_name) + bench_dir = os.path.join(f"{self.traces_dir}", f"{sanitized_bench_name}") os.makedirs(bench_dir, exist_ok=True) @@ -163,6 +167,8 @@ def handle_output(self, unitrace_output: str): shutil.move(os.path.join(options.benchmark_cwd, pid_json_files[-1]), json_name) log.debug(f"Moved {pid_json_files[-1]} to {json_name}") + log.info(f"Unitrace output files: {unitrace_output}, {json_name}") + # Prune old unitrace directories self._prune_unitrace_dirs(os.path.dirname(unitrace_output)) diff --git a/devops/scripts/benchmarks/utils/utils.py b/devops/scripts/benchmarks/utils/utils.py index afae989d4b8b9..16e00a0145faa 100644 --- a/devops/scripts/benchmarks/utils/utils.py +++ b/devops/scripts/benchmarks/utils/utils.py @@ -19,6 +19,19 @@ from utils.logger import log +def sanitize_filename(name: str) -> str: + """ + Sanitize a string to be safe for use as a filename or directory name. + Replace invalid characters with underscores. + Invalid characters: " : < > | * ? \r \n + """ + # Replace invalid characters with underscores + # Added space to list to avoid directories with spaces which cause issues in shell commands + invalid_chars = r'[":;<>|*?\r\n ]' + sanitized = re.sub(invalid_chars, "_", name) + return sanitized + + def run( command, env_vars={}, @@ -156,7 +169,8 @@ def download(dir, url, file, untar=False, unzip=False, checksum=""): if unzip: [stripped_gz, _] = os.path.splitext(data_file) with gzip.open(data_file, "rb") as f_in, open(stripped_gz, "wb") as f_out: - shutil.copyfileobj(f_in, f_out) + # copyfileobj expects binary file-like objects; type checker may complain about union types + shutil.copyfileobj(f_in, f_out) # type: ignore[arg-type] else: log.debug(f"{data_file} exists, skipping...") return data_file diff --git a/devops/scripts/install_build_tools.sh b/devops/scripts/install_build_tools.sh index 39ad259550d6f..1a1aa6dccda63 100755 --- a/devops/scripts/install_build_tools.sh +++ b/devops/scripts/install_build_tools.sh @@ -26,6 +26,8 @@ apt update && apt install -yqq \ curl \ libhwloc-dev \ libzstd-dev \ + linux-tools-generic \ + linux-tools-common \ time # To obtain latest release of spriv-tool.