From 94d8e3c01f38e0eb19727c978965061034d9e594 Mon Sep 17 00:00:00 2001 From: mattip Date: Wed, 27 May 2026 12:26:52 +0300 Subject: [PATCH 1/2] allow up to two environments on timeline and other smaller fixes --- codespeed/static/css/main.css | 1 + codespeed/static/js/changes.js | 14 ++ codespeed/static/js/timeline.js | 224 ++++++++++++++------ codespeed/templates/codespeed/timeline.html | 8 +- codespeed/views.py | 135 +++++++----- codespeed/views_data.py | 18 +- 6 files changed, 267 insertions(+), 133 deletions(-) diff --git a/codespeed/static/css/main.css b/codespeed/static/css/main.css index 361ffa09..a0c538ff 100644 --- a/codespeed/static/css/main.css +++ b/codespeed/static/css/main.css @@ -234,6 +234,7 @@ div.about_content { text-align: left; } .boxbody input { margin-left: 0; vertical-align: top; } #options li { margin-bottom: 0.8em; } .seriescolor { float: right; margin-top: 2px; height: 13px; width: 14px; } +.envlinecolor { float: right; font-size: 12px; line-height: 18px; color: #555; } .compplot-wrap { display: inline-block; } a.togglefold { font-size: normal; color: #000000; } a.togglefold::before { diff --git a/codespeed/static/js/changes.js b/codespeed/static/js/changes.js index 21e5895a..2c93f153 100644 --- a/codespeed/static/js/changes.js +++ b/codespeed/static/js/changes.js @@ -93,6 +93,9 @@ function updateRevisionMarkers(env_id) { }); $("#revision").html(options); $("#revision").val(current); + if (!$("#revision").val()) { + $("#revision").prop('selectedIndex', 0); + } } function refreshContent() { @@ -123,8 +126,19 @@ function changeRevisions() { selected_project = projectmatrix[executable]; if (selected_project !== currentproject) { + var oldRevs = revisiondata[currentproject] || []; + var oldEntry = oldRevs.find(function(r) { return r[1] === $("#revision").val(); }); + var oldDate = oldEntry ? new Date(oldEntry[0].slice(0, 19)) : null; currentproject = selected_project; updateRevisionMarkers($("input[name='environment']:checked").val()); + if (oldDate) { + var newRevs = revisiondata[currentproject] || []; + var best = newRevs.reduce(function(bi, r, i) { + return Math.abs(new Date(r[0].slice(0, 19)) - oldDate) < + Math.abs(new Date(newRevs[bi][0].slice(0, 19)) - oldDate) ? i : bi; + }, 0); + $("#revision").prop('selectedIndex', best); + } //Give visual cue that the select box has changed var bgc = $("#revision").parent().parent().css("backgroundColor"); diff --git a/codespeed/static/js/timeline.js b/codespeed/static/js/timeline.js index 45c40834..6f921856 100644 --- a/codespeed/static/js/timeline.js +++ b/codespeed/static/js/timeline.js @@ -21,6 +21,13 @@ function getColor(exe_id) { return $("#executable > div.boxbody").find("input[value='" + exe_id + "']").data('color'); } +function updateExeColors() { + $("#executable div.boxbody > ul > ul > li > input").each(function() { + var $sq = $(this).parent().find("div.seriescolor"); + $sq.css("background-color", $(this).is(':checked') ? getColor($(this).attr("id").slice(10)) : ''); + }); +} + function shouldPlotEquidistant() { return $("#equidistant").is(':checked'); } function shouldPlotQuartiles() { return $("#show_quartile_bands").is(':checked'); } function shouldPlotExtrema() { return $("#show_extrema_bands").is(':checked'); } @@ -30,7 +37,7 @@ function getConfiguration() { exe: readCheckbox("input[name='executable']:checked"), base: $("#baseline option:selected").val(), ben: $("input[name='benchmark']:checked").val(), - env: $("input[name='environments']:checked").val(), + env: readCheckbox("input[name='environments']:checked"), revs: $("#revisions option:selected").val(), equid: shouldPlotEquidistant() ? "on" : "off", quarts: shouldPlotQuartiles() ? "on" : "off", @@ -61,23 +68,28 @@ function updateUrl() { function destroyPlots() { if (plotInstance) { plotInstance.destroy(); plotInstance = null; } $("#plot").off("mousemove.tooltip"); + $("#plot-legend").remove(); miniplotInstances.forEach(function(g) { g.destroy(); }); miniplotInstances = []; } // Build dygraphs-ready data from the branches dict. -// Returns {labels, colors, data, commitMap, sortedDateKeys, seriesIds} -// Uses customBars: data rows are [x, [low,mid,high], [low,mid,high], ...] -function buildGraphData(branches, median, equidistant) { - var seriesIds = []; - var seriesRaw = {}; // exe_id -> dateKey -> {low, mid, high, commit, tag} +// Returns {labels, colors, data, commitMap, sortedDateKeys, seriesIds, envIdToIndex} +// Series keys are "exe_id:env_id". Uses customBars: [x, [low,mid,high], ...] +function buildGraphData(branches, environments, median, equidistant) { + var envIdToIndex = {}; + (environments || []).forEach(function(env, i) { envIdToIndex[String(env.id)] = i; }); + var multiEnv = (environments || []).length > 1; + + var seriesIds = []; // composite "exe_id:env_id" + var seriesRaw = {}; // composite -> dateKey -> {low, mid, high, commit, tag} var dateIndex = {}; // dateKey -> Date for (var branch in branches) { - for (var exe_id in branches[branch]) { - seriesIds.push(exe_id); - seriesRaw[exe_id] = {}; - var pts = branches[branch][exe_id]; + for (var compositeKey in branches[branch]) { + seriesIds.push(compositeKey); + seriesRaw[compositeKey] = {}; + var pts = branches[branch][compositeKey]; for (var i = 0; i < pts.length; i++) { var pt = pts[i]; var dateKey = pt[0]; @@ -108,10 +120,10 @@ function buildGraphData(branches, median, equidistant) { } dateIndex[dateKey] = new Date(dateKey.trim()); - seriesRaw[exe_id][dateKey] = {low: low, mid: mid, high: high, - commit: commit, tag: tag, - suite_version: suite_version, - dateKey: dateKey}; + seriesRaw[compositeKey][dateKey] = {low: low, mid: mid, high: high, + commit: commit, tag: tag, + suite_version: suite_version, + dateKey: dateKey}; } } } @@ -123,23 +135,33 @@ function buildGraphData(branches, median, equidistant) { var labels = ['Date']; var colors = []; for (var k = 0; k < seriesIds.length; k++) { - var id = seriesIds[k]; - labels.push($("label[for*='executable" + id + "']").text().trim()); - colors.push(getColor(id)); + var ckey = seriesIds[k]; + var kparts = ckey.split(':'); + var exeId = kparts[0]; + var envId = kparts[1] || null; + var exeLabel = $("label[for*='executable" + exeId + "']").text().trim(); + if (multiEnv && envId) { + var envObj = (environments || []).reduce(function(found, e) { + return found || (String(e.id) === envId ? e : null); + }, null); + exeLabel = exeLabel + ' (' + (envObj ? envObj.name : envId) + ')'; + } + labels.push(exeLabel); + colors.push(getColor(exeId)); } - // commitMap[dateKey][exe_id] = {commit, tag} + // commitMap[dateKey][compositeKey] = {commit, tag, low, high, suite_version} var commitMap = {}; - var tsMap = {}; // timestamp (ms) -> dateKey, for legend lookup + var tsMap = {}; // timestamp (ms) -> dateKey, for tooltip lookup sortedDateKeys.forEach(function(dk) { tsMap[dateIndex[dk].getTime()] = dk; commitMap[dk] = {}; - seriesIds.forEach(function(id) { - if (seriesRaw[id][dk]) { - var pt = seriesRaw[id][dk]; - commitMap[dk][id] = {commit: pt.commit, tag: pt.tag, - low: pt.low, high: pt.high, - suite_version: pt.suite_version}; + seriesIds.forEach(function(ckey) { + if (seriesRaw[ckey][dk]) { + var pt = seriesRaw[ckey][dk]; + commitMap[dk][ckey] = {commit: pt.commit, tag: pt.tag, + low: pt.low, high: pt.high, + suite_version: pt.suite_version}; } }); }); @@ -147,8 +169,8 @@ function buildGraphData(branches, median, equidistant) { var graphData = sortedDateKeys.map(function(dk, idx) { var xval = equidistant ? idx : dateIndex[dk]; var row = [xval]; - seriesIds.forEach(function(id) { - var pt = seriesRaw[id][dk]; + seriesIds.forEach(function(ckey) { + var pt = seriesRaw[ckey][dk]; row.push(pt ? [pt.low, pt.mid, pt.high] : null); }); return row; @@ -156,7 +178,8 @@ function buildGraphData(branches, median, equidistant) { return {labels: labels, colors: colors, data: graphData, commitMap: commitMap, sortedDateKeys: sortedDateKeys, - seriesIds: seriesIds, tsMap: tsMap, dateIndex: dateIndex}; + seriesIds: seriesIds, tsMap: tsMap, dateIndex: dateIndex, + envIdToIndex: envIdToIndex}; } function renderPlot(data) { @@ -173,7 +196,7 @@ function renderPlot(data) { $("span.options.median").css("display", "inline"); } - var built = buildGraphData(data.branches, median, equidistant); + var built = buildGraphData(data.branches, data.environments || [], median, equidistant); if (built.data.length === 0) { $("#plot").html(getLoadText("No data available", 420)); return; @@ -196,7 +219,6 @@ function renderPlot(data) { var seriesIds = built.seriesIds; var tsMap = built.tsMap; var dateIndex = built.dateIndex; - var env = $("input[name='environments']:checked").val(); // Find x-positions where suite_version changes between adjacent points var seen = {}; @@ -204,9 +226,9 @@ function renderPlot(data) { sortedDateKeys.forEach(function(dk, idx) { if (idx === 0) { return; } var prevDk = sortedDateKeys[idx - 1]; - seriesIds.forEach(function(id) { - var curr = commitMap[dk] && commitMap[dk][id]; - var prev = commitMap[prevDk] && commitMap[prevDk][id]; + seriesIds.forEach(function(ckey) { + var curr = commitMap[dk] && commitMap[dk][ckey]; + var prev = commitMap[prevDk] && commitMap[prevDk][ckey]; if (curr && prev && curr.suite_version && prev.suite_version && curr.suite_version !== prev.suite_version && !seen[dk]) { seen[dk] = true; @@ -215,12 +237,12 @@ function renderPlot(data) { }); }); - // Map label -> color and label -> exeId for tooltip + // Map label -> color and label -> composite series key for tooltip var colorMap = {}; - var labelToExeId = {}; + var labelToSeriesKey = {}; built.labels.slice(1).forEach(function(lbl, i) { colorMap[lbl] = built.colors[i]; - labelToExeId[lbl] = seriesIds[i]; + labelToSeriesKey[lbl] = seriesIds[i]; }); function lookupDateKey(x) { @@ -239,40 +261,61 @@ function renderPlot(data) { function legendFormatter(ld) { if (ld.x === undefined) { return ''; } var dk = lookupDateKey(ld.x); - var html = '
' + - (dk ? dk.slice(0, 16) : '') + '
'; + + // Collect commit info from the first visible series with data + var commitInfo = null; + ld.series.forEach(function(s) { + if (commitInfo || !s.isVisible || s.y === undefined) { return; } + var seriesKey = labelToSeriesKey[s.labelHTML] || labelToSeriesKey[s.name]; + if (dk && commitMap[dk] && commitMap[dk][seriesKey]) { + commitInfo = commitMap[dk][seriesKey]; + } + }); + + var header = '
' + + (dk ? dk.slice(0, 16) : ''); + if (commitInfo) { + header += ' commit: ' + commitInfo.commit; + if (commitInfo.tag) { header += '  tag: ' + commitInfo.tag; } + if (versionBoundaries.length > 0 && commitInfo.suite_version) { header += '  suite: ' + commitInfo.suite_version; } + header += ''; + } + header += '
'; + + var html = header; ld.series.forEach(function(s) { if (!s.isVisible || s.y === undefined) { return; } - var exeId = labelToExeId[s.labelHTML] || labelToExeId[s.name]; + var seriesKey = labelToSeriesKey[s.labelHTML] || labelToSeriesKey[s.name]; + if (!seriesKey) { return; } // skip baseline var color = colorMap[s.labelHTML] || colorMap[s.name] || '#333'; html += '
' + '' + s.labelHTML + ': ' + s.y.toPrecision(4) + ''; - if (dk && commitMap[dk] && commitMap[dk][exeId]) { - var info = commitMap[dk][exeId]; + if (dk && commitMap[dk] && commitMap[dk][seriesKey]) { + var info = commitMap[dk][seriesKey]; if (info.low !== info.high) { html += ' [' + info.low.toPrecision(3) + ' \u2013 ' + info.high.toPrecision(3) + ']'; } - html += '
' + - 'commit: ' + info.commit; - if (info.tag) { html += '  tag: ' + info.tag; } - if (versionBoundaries.length > 0 && info.suite_version) { html += '  suite: ' + info.suite_version; } - html += ''; } html += '
'; }); return html; } - // Per-series options: baseline gets thinner line, no points + // Per-series options: baseline gets thinner line; 2nd env gets dashed var seriesOpts = {}; built.labels.slice(1).forEach(function(lbl, i) { var isBase = hasBaseline && i === built.labels.length - 2; + var ckey = built.seriesIds[i] || ''; + var envId = ckey.split(':')[1] || null; + var envIdx = envId ? (built.envIdToIndex[envId] || 0) : 0; seriesOpts[lbl] = { strokeWidth: isBase ? 1.5 : 2, + strokePattern: (!isBase && envIdx === 1) ? [6, 3] : null, drawPoints: !isBase, pointSize: 3, + highlightCircleSize: isBase ? 0 : 5, color: built.colors[i] }; }); @@ -363,16 +406,38 @@ function renderPlot(data) { }, sortedDateKeys[0]); } if (!dk || !commitMap[dk]) { return; } - var id = seriesIds[0]; - var info = commitMap[dk][id]; + var highlightedLabel = plotInstance && plotInstance.getHighlightSeries && plotInstance.getHighlightSeries(); + var ckey = (highlightedLabel && labelToSeriesKey[highlightedLabel]) || seriesIds[0]; + var info = commitMap[dk][ckey]; if (info) { + var parts = ckey.split(':'); + var navEnv = parts[1] || $("input[name='environments']:checked").first().val(); window.location = CHANGES_URL + "?rev=" + info.commit + - "&exe=" + id + "&env=" + env; + "&exe=" + parts[0] + "&env=" + navEnv; } } } ); + // Persistent legend below the plot + (function() { + var $legend = $('
'); + built.labels.slice(1).forEach(function(lbl, i) { + var isBase = hasBaseline && i === built.labels.length - 2; + var ckey = built.seriesIds[i] || ''; + var envId = ckey.split(':')[1] || null; + var envIdx = envId ? (built.envIdToIndex[envId] || 0) : 0; + var color = built.colors[i]; + var dashed = !isBase && envIdx === 1; + var svg = '' + + ''; + $legend.append('' + + svg + '' + lbl + ''); + }); + $("#plot").after($legend); + }()); + $("#plot").on("mousemove.tooltip", function(e) { var $tip = $("#dygraph-tooltip"); if (!lastHighlightPoints) { $tip.hide(); return; } @@ -408,7 +473,7 @@ function renderPlot(data) { function renderMiniplot(plotid, data) { var median = data['data_type'] === 'M'; - var built = buildGraphData(data.branches, median, false); + var built = buildGraphData(data.branches, data.environments || [], median, false); if (built.data.length === 0) { return; } if (data.baseline !== "None") { @@ -427,6 +492,18 @@ function renderMiniplot(plotid, data) { : [mxv - 1, mxv + 1]; } + var miniSeriesOpts = {}; + built.labels.slice(1).forEach(function(lbl, i) { + var ckey = built.seriesIds[i] || ''; + var envId = ckey.split(':')[1] || null; + var envIdx = envId ? (built.envIdToIndex[envId] || 0) : 0; + miniSeriesOpts[lbl] = { + strokeWidth: 1.5, + strokePattern: envIdx === 1 ? [4, 3] : null, + color: built.colors[i] + }; + }); + var g = new Dygraph( document.getElementById(plotid), built.data, @@ -436,6 +513,7 @@ function renderMiniplot(plotid, data) { labels: built.labels, colors: built.colors, customBars: true, + series: miniSeriesOpts, legend: 'never', axes: { x: { drawAxis: false, drawGrid: false }, @@ -476,7 +554,7 @@ function render(data) { config.nextBenchmarks = data.nextBenchmarks; $.getJSON("json/", config, render); } - $("#revisions").attr("disabled", true); + $("#revisions").val(15).attr("disabled", true); $("#equidistant").attr("disabled", true); for (var i = 0; i < data.timelines.length; i++) { var tl = data.timelines[i]; @@ -529,13 +607,14 @@ function setValuesOfInputFields(params) { var benchmark = valueOrDefault(params.ben, defaults.benchmark); $("input:radio[name='benchmark']").filter("[value='" + benchmark + "']").prop('checked', true); - var environment = valueOrDefault(params.env, defaults.environment); - $("input:radio[name='environments']").filter("[value='" + environment + "']").prop('checked', true); - - $("#executable div.boxbody > ul > ul > li > input").each(function() { - $(this).parent().find("div.seriescolor") - .css("background-color", getColor($(this).attr("id").slice(10))); + var envDefault = (defaults.environments || []).map(String).join(','); + var envIds = valueOrDefault(params.env, envDefault).split(',').filter(Boolean); + var envSel = $("input[name='environments']"); + envIds.forEach(function(envId) { + envSel.filter("[value='" + envId.trim() + "']").prop('checked', true); }); + updateEnvIndicators(); + updateExeColors(); $("#baselinecolor").css("background-color", baselineColor); $("#equidistant").prop('checked', valueOrDefault(params.equid, defaults.equidistant) === "on"); @@ -543,18 +622,36 @@ function setValuesOfInputFields(params) { $("#show_extrema_bands").prop('checked', valueOrDefault(params.extr, defaults.extrema) === "on"); } +function updateEnvIndicators() { + $("input[name='environments']").each(function() { + $(this).siblings('.envlinecolor').text(''); + }); + $("input[name='environments']:checked").each(function(i) { + $(this).siblings('.envlinecolor').text(i === 0 ? '───' : '╌╌╌'); + }); +} + function initializeSite() { var params = getUrlParams(); - setValuesOfInputFields(params); setExeColors(); + setValuesOfInputFields(params); var onChange = function() { updateUrl(); refreshContent(); }; $("#revisions, #baseline").change(onChange); - $("input[name='executable'], input[name='branch'], input[name='benchmark']," + - "input[name='environments'], #equidistant, #show_quartile_bands, #show_extrema_bands" + $("input[name='branch'], input[name='benchmark']," + + "#equidistant, #show_quartile_bands, #show_extrema_bands" ).change(onChange); - $('.checkall, .uncheckall').click(onChange); + $("input[name='executable']").change(function() { updateExeColors(); onChange(); }); + $("input[name='environments']").change(function() { + if ($("input[name='environments']:checked").length > 2) { + $(this).prop('checked', false); + return; + } + updateEnvIndicators(); + onChange(); + }); + $('.checkall, .uncheckall').click(function() { updateExeColors(); onChange(); }); $("#permalink").click(function() { window.location = "?" + $.param(getConfiguration()); @@ -570,6 +667,9 @@ function initializeSite() { function init(def) { defaults = def; + if (!defaults.environments) { + defaults.environments = defaults.environment ? [defaults.environment] : []; + } $.ajaxSetup({cache: false}); initializeSite(); } diff --git a/codespeed/templates/codespeed/timeline.html b/codespeed/templates/codespeed/timeline.html index cadfa506..71fa46a0 100644 --- a/codespeed/templates/codespeed/timeline.html +++ b/codespeed/templates/codespeed/timeline.html @@ -23,8 +23,8 @@ @@ -77,7 +77,7 @@
- + Show the last