1
0
mirror of https://github.com/ohwgiles/laminar.git synced 2024-10-27 20:34:20 +00:00

frontend: better dynamic update of graphs

react to jobCompletion messages by updating graphs in
more cases so manual refresh is not necessary to see
most up to date representation.
This commit is contained in:
Oliver Giles 2021-07-09 10:03:30 +12:00
parent 60f7ee5402
commit a50514a135
2 changed files with 102 additions and 23 deletions

View File

@ -470,6 +470,12 @@ std::string Laminar::getStatus(MonitorScope scope) {
j.EndObject();
});
j.EndArray();
j.startObject("completedCounts");
db->stmt("SELECT name, COUNT(*) FROM builds WHERE result IS NOT NULL GROUP BY name")
.fetch<str, uint>([&](str job, uint count){
j.set(job.c_str(), count);
});
j.EndObject();
}
j.EndObject();
return j.str();

View File

@ -90,9 +90,9 @@ Vue.mixin({
let m = d.getMinutes();
if (m < 10)
m = '0' + m;
return d.getHours() + ':' + m + ' on ' +
return d.getHours() + ':' + m + ' on ' +
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + d.getDate() + '. ' +
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()] + ' ' +
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()] + ' ' +
d.getFullYear();
},
// Pretty-print a duration
@ -203,15 +203,19 @@ const Charts = (() => {
if (c.data.labels[j] == name) {
c.data.datasets[0].data[j]++;
c.update();
break;
return;
}
}
// if we get here, it's a new/unknown job
c.data.labels.push(name);
c.data.datasets[0].data.push(1);
c.update();
}
return c;
},
createTimePerJobChart: (id, data) => {
createTimePerJobChart: (id, data, completedCounts) => {
const scale = timeScale(Math.max(...Object.values(data)));
return new Chart(document.getElementById(id), {
const c = new Chart(document.getElementById(id), {
type: 'horizontalBar',
data: {
labels: Object.keys(data),
@ -232,23 +236,38 @@ const Charts = (() => {
}
}]},
tooltips:{callbacks:{
label: (tip, data) => data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel + ' ' + scale.label.toLowerCase()
label: (tip, data) => data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel.toFixed(2) + ' ' + scale.label.toLowerCase()
}}
}
});
c.jobCompleted = (name, time) => {
for (var j = 0; j < c.data.datasets[0].data.length; ++j) {
if (c.data.labels[j] == name) {
c.data.datasets[0].data[j] = ((completedCounts[name]-1) * c.data.datasets[0].data[j] + time * scale.factor) / completedCounts[name];
c.update();
return;
}
}
// if we get here, it's a new/unknown job
c.data.labels.push(name);
c.data.datasets[0].data.push(time * scale.factor);
c.update();
};
return c;
},
createRunTimeChangesChart: (id, data) => {
const scale = timeScale(Math.max(...data.map(e => Math.max(...e.durations))));
return new Chart(document.getElementById(id), {
const dataValue = (name, durations) => ({
label: name,
data: durations.map(x => x * scale.factor),
borderColor: 'hsl('+(name.hashCode() % 360)+', 27%, 57%)',
backgroundColor: 'transparent'
});
const c = new Chart(document.getElementById(id), {
type: 'line',
data: {
labels: [...Array(10).keys()],
datasets: data.map(e => ({
label: e.name,
data: e.durations.map(x => x * scale.factor),
borderColor: 'hsl('+(e.name.hashCode() % 360)+', 27%, 57%)',
backgroundColor: 'transparent'
}))
datasets: data.map(e => dataValue(e.name, e.durations))
},
options:{
title: { display: true, text: 'Run time changes' },
@ -268,10 +287,25 @@ const Charts = (() => {
}
}
});
c.jobCompleted = (name, time) => {
for (var j = 0; j < c.data.datasets.length; ++j) {
if (c.data.datasets[j].label == name) {
if(c.data.datasets[j].data.length == 10)
c.data.datasets[j].data.shift();
c.data.datasets[j].data.push(time * scale.factor);
c.update();
return;
}
}
// if we get here, it's a new/unknown job
c.data.datasets.push(dataValue(name, [time]));
c.update();
};
return c;
},
createRunTimeChart: (id, jobs, avg) => {
const scale = timeScale(Math.max(...jobs.map(v=>v.completed-v.started)));
return new Chart(document.getElementById(id), {
const c = new Chart(document.getElementById(id), {
type: 'bar',
data: {
labels: jobs.map(e => '#' + e.number).reverse(),
@ -319,6 +353,22 @@ const Charts = (() => {
}}
}
});
c.jobCompleted = (num, result, time) => {
let avg = c.data.datasets[0].data[0].y / scale.factor;
avg = ((avg * (num - 1)) + time) / num;
c.data.datasets[0].data[0].y = avg * scale.factor;
c.data.datasets[0].data[1].y = avg * scale.factor;
if(c.data.datasets[1].data.length == 20) {
c.data.labels.shift();
c.data.datasets[1].data.shift();
c.data.datasets[1].backgroundColor.shift();
}
c.data.labels.push('#' + num);
c.data.datasets[1].data.push(time * scale.factor);
c.data.datasets[1].backgroundColor.push(result == 'success' ? '#74af77': '#883d3d');
c.update();
};
return c;
}
};
})();
@ -341,6 +391,7 @@ const Home = templateId => {
lowPassRates: [],
};
let chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
let completedCounts;
return {
template: templateId,
data: () => state,
@ -351,6 +402,7 @@ const Home = templateId => {
state.jobsRecent = msg.recent;
state.resultChanged = msg.resultChanged;
state.lowPassRates = msg.lowPassRates;
completedCounts = msg.completedCounts;
this.$forceUpdate();
// defer charts to nextTick because they get DOM elements which aren't rendered yet
@ -358,7 +410,7 @@ const Home = templateId => {
chtUtilization = Charts.createExecutorUtilizationChart("chartUtil", msg.executorsBusy, msg.executorsTotal);
chtBuildsPerDay = Charts.createRunsPerDayChart("chartBpd", msg.buildsPerDay);
chtBuildsPerJob = Charts.createRunsPerJobChart("chartBpj", msg.buildsPerJob);
chtTimePerJob = Charts.createTimePerJobChart("chartTpj", msg.timePerJob);
chtTimePerJob = Charts.createTimePerJobChart("chartTpj", msg.timePerJob, completedCounts);
chtBuildTimeChanges = Charts.createRunTimeChangesChart("chartBuildTimeChanges", msg.buildTimeChanges);
});
},
@ -373,8 +425,10 @@ const Home = templateId => {
chtUtilization.executorBusyChanged(true);
},
job_completed: function(data) {
for (var i = 0; i < state.jobsRunning.length; ++i) {
var job = state.jobsRunning[i];
if(!(job.name in completedCounts))
completedCounts[job.name] = 0;
for(let i = 0; i < state.jobsRunning.length; ++i) {
const job = state.jobsRunning[i];
if (job.name == data.name && job.number == data.number) {
state.jobsRunning.splice(i, 1);
state.jobsRecent.splice(0, 0, data);
@ -382,9 +436,28 @@ const Home = templateId => {
break;
}
}
for(let i = 0; i < state.resultChanged.length; ++i) {
const job = state.resultChanged[i];
if(job.name == data.name) {
job[data.result === 'success' ? 'lastSuccess' : 'lastFailure'] = data.number;
this.$forceUpdate();
break;
}
}
for(let i = 0; i < state.lowPassRates.length; ++i) {
const job = state.lowPassRates[i];
if(job.name == data.name) {
job.passRate = ((completedCounts[job.name] - 1) * job.passRate + (data.result === 'success' ? 1 : 0)) / completedCounts[job.name];
this.$forceUpdate();
break;
}
}
completedCounts[job.name]++;
chtBuildsPerDay.jobCompleted(data.result === 'success')
chtUtilization.executorBusyChanged(false);
chtBuildsPerJob.jobCompleted(data.name)
chtBuildsPerJob.jobCompleted(data.name);
chtTimePerJob.jobCompleted(data.name, data.completed - data.started);
chtBuildTimeChanges.jobCompleted(data.name, data.completed - data.started);
}
}
};
@ -494,7 +567,7 @@ const Job = templateId => {
pages: 0,
sort: {}
};
let chtBt = null;
let chtBuildTime = null;
return {
template: templateId,
props: ['route'],
@ -512,12 +585,12 @@ const Job = templateId => {
// "status" comes again if we change page/sorting. Delete the
// old chart and recreate it to prevent flickering of old data
if(chtBt)
chtBt.destroy();
if(chtBuildTime)
chtBuildTime.destroy();
// defer chart to nextTick because they get DOM elements which aren't rendered yet
this.$nextTick(() => {
chtBt = Charts.createRunTimeChart("chartBt", msg.recent, msg.averageRuntime);
chtBuildTime = Charts.createRunTimeChart("chartBt", msg.recent, msg.averageRuntime);
});
},
job_queued: function() {
@ -534,8 +607,8 @@ const Job = templateId => {
state.jobsRunning.splice(i, 1);
state.jobsRecent.splice(0, 0, data);
this.$forceUpdate();
// TODO: update the chart
}
chtBuildTime.jobCompleted(data.number, data.result, data.completed - data.started);
},
page_next: function() {
state.sort.page++;