mirror of
https://github.com/mperham/sidekiq.git
synced 2022-11-09 13:52:34 -05:00
Histogram chart for job-specific metrics (#5473)
* Refactor job query * First attempt at a histogram chart * Explore a box plot chart * Show 3 chart variations * Outline boxes instead of solid boxes * Remove box plot chart * Use linear y-axis This matches the axis for the metrics overview page, and it clarifies the data. * Data tables for job metrics * Add histogram totals chart * Move things around * Tooltip for histogram chart * Fix deploy tooltip * Extract marks query * Extract chart base class * Renaming * Ensure a min radius for histogram bubbles High job counts can result in a very small multiplier, which was making some of the bubbles too small to be visible. * Round everything to two decimals for consistency * styling for metrics headers * Show emdash when timing info is n/a * No job results found message * No need for metrics header
This commit is contained in:
parent
ff7e41924d
commit
0b3751bf29
8 changed files with 343 additions and 177 deletions
|
@ -60,57 +60,40 @@ module Sidekiq
|
||||||
time -= 60
|
time -= 60
|
||||||
end
|
end
|
||||||
|
|
||||||
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
||||||
result_range = result.starts_at..result.ends_at
|
|
||||||
marks.each do |timestamp, label|
|
|
||||||
time = Time.parse(timestamp)
|
|
||||||
if result_range.cover? time
|
|
||||||
result.marks << MarkResult.new(time, label)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def for_job(klass)
|
def for_job(klass, minutes: 60)
|
||||||
resultset = {}
|
result = Result.new
|
||||||
resultset[:date] = @time.to_date
|
|
||||||
resultset[:period] = :hour
|
|
||||||
resultset[:ends_at] = @time
|
|
||||||
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
|
||||||
|
|
||||||
time = @time
|
time = @time
|
||||||
initial = @pool.with do |conn|
|
redis_results = @pool.with do |conn|
|
||||||
conn.pipelined do |pipe|
|
conn.pipelined do |pipe|
|
||||||
resultset[:size] = 60
|
minutes.times do |idx|
|
||||||
60.times do |idx|
|
key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
|
||||||
key = "j|#{time.strftime("%Y%m%d|%-H:%-M")}"
|
|
||||||
pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
|
pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
|
||||||
|
result.prepend_bucket time
|
||||||
time -= 60
|
time -= 60
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
time = @time
|
time = @time
|
||||||
hist = Histogram.new(klass)
|
@pool.with do |conn|
|
||||||
results = @pool.with do |conn|
|
redis_results.each do |(ms, p, f)|
|
||||||
initial.map do |(ms, p, f)|
|
result.job_results[klass].add_metric "ms", time, ms.to_i if ms
|
||||||
tm = Time.utc(time.year, time.month, time.mday, time.hour, time.min, 0)
|
result.job_results[klass].add_metric "p", time, p.to_i if p
|
||||||
{
|
result.job_results[klass].add_metric "f", time, f.to_i if f
|
||||||
time: tm.iso8601,
|
result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time)
|
||||||
epoch: tm.to_i,
|
time -= 60
|
||||||
ms: ms.to_i, p: p.to_i, f: f.to_i, hist: hist.fetch(conn, time)
|
|
||||||
}.tap { |x|
|
|
||||||
x[:mark] = marks[x[:time]] if marks[x[:time]]
|
|
||||||
time -= 60
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resultset[:marks] = marks
|
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
||||||
resultset[:starts_at] = time
|
|
||||||
resultset[:data] = results
|
result
|
||||||
resultset
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
|
class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
|
||||||
|
@ -128,20 +111,37 @@ module Sidekiq
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class JobResult < Struct.new(:series, :totals)
|
class JobResult < Struct.new(:series, :hist, :totals)
|
||||||
def initialize
|
def initialize
|
||||||
super
|
super
|
||||||
self.series = Hash.new { |h, k| h[k] = {} }
|
self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
|
||||||
|
self.hist = Hash.new { |h, k| h[k] = [] }
|
||||||
self.totals = Hash.new(0)
|
self.totals = Hash.new(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_metric(metric, time, value)
|
def add_metric(metric, time, value)
|
||||||
totals[metric] += value
|
totals[metric] += value
|
||||||
series[metric][time.strftime("%H:%M")] = value
|
series[metric][time.strftime("%H:%M")] += value
|
||||||
|
|
||||||
# Include timing measurements in seconds for convenience
|
# Include timing measurements in seconds for convenience
|
||||||
add_metric("s", time, value / 1000.0) if metric == "ms"
|
add_metric("s", time, value / 1000.0) if metric == "ms"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_hist(time, hist_result)
|
||||||
|
hist[time.strftime("%H:%M")] = hist_result
|
||||||
|
end
|
||||||
|
|
||||||
|
def total_avg(metric = "ms")
|
||||||
|
completed = totals["p"] - totals["f"]
|
||||||
|
totals[metric].to_f / completed
|
||||||
|
end
|
||||||
|
|
||||||
|
def series_avg(metric = "ms")
|
||||||
|
series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
|
||||||
|
completed = series.dig("p", bucket) - series.dig("f", bucket)
|
||||||
|
result[bucket] = completed == 0 ? 0 : value.to_f / completed
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class MarkResult < Struct.new(:time, :label)
|
class MarkResult < Struct.new(:time, :label)
|
||||||
|
@ -149,6 +149,21 @@ module Sidekiq
|
||||||
time.strftime("%H:%M")
|
time.strftime("%H:%M")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_marks(time_range)
|
||||||
|
[].tap do |result|
|
||||||
|
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
||||||
|
|
||||||
|
marks.each do |timestamp, label|
|
||||||
|
time = Time.parse(timestamp)
|
||||||
|
if time_range.cover? time
|
||||||
|
result << MarkResult.new(time, label)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,7 +69,7 @@ module Sidekiq
|
||||||
get "/metrics/:name" do
|
get "/metrics/:name" do
|
||||||
@name = route_params[:name]
|
@name = route_params[:name]
|
||||||
q = Sidekiq::Metrics::Query.new
|
q = Sidekiq::Metrics::Query.new
|
||||||
@resultset = q.for_job(@name)
|
@query_result = q.for_job(@name)
|
||||||
erb(:metrics_for_job)
|
erb(:metrics_for_job)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -48,9 +48,9 @@ describe Sidekiq::Metrics do
|
||||||
|
|
||||||
q = Sidekiq::Metrics::Query.new(now: whence)
|
q = Sidekiq::Metrics::Query.new(now: whence)
|
||||||
rs = q.for_job("FooJob")
|
rs = q.for_job("FooJob")
|
||||||
refute_nil rs[:marks]
|
refute_nil rs.marks
|
||||||
assert_equal 1, rs[:marks].size
|
assert_equal 1, rs.marks.size
|
||||||
assert_equal "cafed00d - some git summary line", rs[:marks][floor], rs.inspect
|
assert_equal "cafed00d - some git summary line", rs.marks.first.label, rs.marks.inspect
|
||||||
|
|
||||||
d = Sidekiq::Metrics::Deploy.new
|
d = Sidekiq::Metrics::Deploy.new
|
||||||
rs = d.fetch(whence)
|
rs = d.fetch(whence)
|
||||||
|
@ -93,12 +93,12 @@ describe Sidekiq::Metrics do
|
||||||
q = Sidekiq::Metrics::Query.new(now: fixed_time)
|
q = Sidekiq::Metrics::Query.new(now: fixed_time)
|
||||||
result = q.top_jobs
|
result = q.top_jobs
|
||||||
assert_equal 60, result.buckets.size
|
assert_equal 60, result.buckets.size
|
||||||
assert_equal({}, result.job_results)
|
assert_equal([], result.job_results.keys)
|
||||||
|
|
||||||
q = Sidekiq::Metrics::Query.new(now: fixed_time)
|
q = Sidekiq::Metrics::Query.new(now: fixed_time)
|
||||||
rs = q.for_job("DoesntExist")
|
result = q.for_job("DoesntExist")
|
||||||
refute_nil rs
|
assert_equal 60, result.buckets.size
|
||||||
assert_equal 7, rs.size
|
assert_equal(["DoesntExist"], result.job_results.keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fetches top job data" do
|
it "fetches top job data" do
|
||||||
|
@ -119,12 +119,15 @@ describe Sidekiq::Metrics do
|
||||||
assert_equal "22:03", result.buckets.last
|
assert_equal "22:03", result.buckets.last
|
||||||
|
|
||||||
assert_equal %w[App::SomeJob App::FooJob].sort, result.job_results.keys.sort
|
assert_equal %w[App::SomeJob App::FooJob].sort, result.job_results.keys.sort
|
||||||
some_job_result = result.job_results["App::SomeJob"]
|
job_result = result.job_results["App::SomeJob"]
|
||||||
refute_nil some_job_result
|
refute_nil job_result
|
||||||
assert_equal %w[p f ms s].sort, some_job_result.series.keys.sort
|
assert_equal %w[p f ms s].sort, job_result.series.keys.sort
|
||||||
assert_equal %w[p f ms s].sort, some_job_result.totals.keys.sort
|
assert_equal %w[p f ms s].sort, job_result.totals.keys.sort
|
||||||
assert_equal 2, some_job_result.series.dig("p", "22:03")
|
assert_equal 2, job_result.series.dig("p", "22:03")
|
||||||
assert_equal 3, some_job_result.totals["p"]
|
assert_equal 3, job_result.totals["p"]
|
||||||
|
# Execution time is not consistent, so these assertions are not exact
|
||||||
|
assert job_result.total_avg("ms").between?(0.5, 2), job_result.total_avg("ms")
|
||||||
|
assert job_result.series_avg("s")["22:03"].between?(0.0005, 0.002), job_result.series_avg("s")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fetches job-specific data" do
|
it "fetches job-specific data" do
|
||||||
|
@ -133,20 +136,27 @@ describe Sidekiq::Metrics do
|
||||||
d.mark(at: fixed_time - 300, label: "cafed00d - some git summary line")
|
d.mark(at: fixed_time - 300, label: "cafed00d - some git summary line")
|
||||||
|
|
||||||
q = Sidekiq::Metrics::Query.new(now: fixed_time)
|
q = Sidekiq::Metrics::Query.new(now: fixed_time)
|
||||||
rs = q.for_job("App::FooJob")
|
result = q.for_job("App::FooJob")
|
||||||
assert_equal Date.new(2022, 7, 22), rs[:date]
|
assert_equal fixed_time - 59 * 60, result.starts_at
|
||||||
assert_equal 60, rs[:data].size
|
assert_equal fixed_time, result.ends_at
|
||||||
assert_equal ["2022-07-22T21:58:00Z", "cafed00d - some git summary line"], rs[:marks].first
|
assert_equal 1, result.marks.size
|
||||||
|
assert_equal "cafed00d - some git summary line", result.marks[0].label
|
||||||
|
assert_equal "21:58", result.marks[0].bucket
|
||||||
|
|
||||||
data = rs[:data]
|
assert_equal 60, result.buckets.size
|
||||||
assert_equal({time: "2022-07-22T22:03:00Z", p: 1, f: 0}, data[0].slice(:time, :p, :f))
|
assert_equal "21:04", result.buckets.first
|
||||||
assert_equal({time: "2022-07-22T22:02:00Z", p: 3, f: 0}, data[1].slice(:time, :p, :f))
|
assert_equal "22:03", result.buckets.last
|
||||||
assert_equal "cafed00d - some git summary line", data[5][:mark]
|
|
||||||
|
|
||||||
# from create_known_data
|
# from create_known_data
|
||||||
hist = data[1][:hist]
|
assert_equal %w[App::FooJob], result.job_results.keys
|
||||||
assert_equal 2, hist[0]
|
job_result = result.job_results["App::FooJob"]
|
||||||
assert_equal 1, hist[1]
|
refute_nil job_result
|
||||||
|
assert_equal %w[p ms s].sort, job_result.series.keys.sort
|
||||||
|
assert_equal %w[p ms s].sort, job_result.totals.keys.sort
|
||||||
|
assert_equal 1, job_result.series.dig("p", "22:03")
|
||||||
|
assert_equal 4, job_result.totals["p"]
|
||||||
|
assert_equal 2, job_result.hist.dig("22:02", 0)
|
||||||
|
assert_equal 1, job_result.hist.dig("22:02", 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,20 +3,17 @@ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
Chart.defaults.color = "#aaa"
|
Chart.defaults.color = "#aaa"
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetricsChart {
|
class BaseChart {
|
||||||
constructor(id, options) {
|
constructor(id, options) {
|
||||||
this.ctx = document.getElementById(id);
|
this.ctx = document.getElementById(id);
|
||||||
this.series = options.series;
|
this.options = options
|
||||||
this.marks = options.marks;
|
|
||||||
this.labels = options.labels;
|
|
||||||
this.swatches = [];
|
|
||||||
this.fallbackColor = "#999";
|
this.fallbackColor = "#999";
|
||||||
this.colors = [
|
this.colors = [
|
||||||
// Colors taken from https://www.chartjs.org/docs/latest/samples/utils.html
|
// Colors taken from https://www.chartjs.org/docs/latest/samples/utils.html
|
||||||
|
"#537bc4",
|
||||||
"#4dc9f6",
|
"#4dc9f6",
|
||||||
"#f67019",
|
"#f67019",
|
||||||
"#f53794",
|
"#f53794",
|
||||||
"#537bc4",
|
|
||||||
"#acc236",
|
"#acc236",
|
||||||
"#166a8f",
|
"#166a8f",
|
||||||
"#00a950",
|
"#00a950",
|
||||||
|
@ -25,15 +22,30 @@ class MetricsChart {
|
||||||
"#991b1b",
|
"#991b1b",
|
||||||
];
|
];
|
||||||
|
|
||||||
const datasets = Object.entries(this.series)
|
|
||||||
.filter(([kls, _]) => options.visible.includes(kls))
|
|
||||||
.map(([kls, _]) => this.dataset(kls));
|
|
||||||
|
|
||||||
this.chart = new Chart(this.ctx, {
|
this.chart = new Chart(this.ctx, {
|
||||||
type: "line",
|
type: this.options.chartType,
|
||||||
data: { labels: this.labels, datasets: datasets },
|
data: { labels: this.options.labels, datasets: this.datasets },
|
||||||
options: this.chartOptions,
|
options: this.chartOptions,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addMarksToChart() {
|
||||||
|
this.options.marks.forEach(([bucket, label], i) => {
|
||||||
|
this.chart.options.plugins.annotation.annotations[`deploy-${i}`] = {
|
||||||
|
type: "line",
|
||||||
|
xMin: bucket,
|
||||||
|
xMax: bucket,
|
||||||
|
borderColor: "rgba(220, 38, 38, 0.4)",
|
||||||
|
borderWidth: 2,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JobMetricsOverviewChart extends BaseChart {
|
||||||
|
constructor(id, options) {
|
||||||
|
super(id, { ...options, chartType: "line" });
|
||||||
|
this.swatches = [];
|
||||||
|
|
||||||
this.addMarksToChart();
|
this.addMarksToChart();
|
||||||
this.chart.update();
|
this.chart.update();
|
||||||
|
@ -71,7 +83,7 @@ class MetricsChart {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: kls,
|
label: kls,
|
||||||
data: this.series[kls],
|
data: this.options.series[kls],
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
|
@ -79,16 +91,10 @@ class MetricsChart {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
addMarksToChart() {
|
get datasets() {
|
||||||
this.marks.forEach(([bucket, label], i) => {
|
return Object.entries(this.options.series)
|
||||||
this.chart.options.plugins.annotation.annotations[`deploy-${i}`] = {
|
.filter(([kls, _]) => this.options.visible.includes(kls))
|
||||||
type: "line",
|
.map(([kls, _]) => this.dataset(kls));
|
||||||
xMin: bucket,
|
|
||||||
xMax: bucket,
|
|
||||||
borderColor: "rgba(220, 38, 38, 0.4)",
|
|
||||||
borderWidth: 2,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get chartOptions() {
|
get chartOptions() {
|
||||||
|
@ -117,7 +123,135 @@ class MetricsChart {
|
||||||
`${item.dataset.label}: ${item.parsed.y.toFixed(1)} seconds`,
|
`${item.dataset.label}: ${item.parsed.y.toFixed(1)} seconds`,
|
||||||
footer: (items) => {
|
footer: (items) => {
|
||||||
const bucket = items[0].label;
|
const bucket = items[0].label;
|
||||||
const marks = this.marks.filter(([b, _]) => b == bucket);
|
const marks = this.options.marks.filter(([b, _]) => b == bucket);
|
||||||
|
return marks.map(([b, msg]) => `Deploy: ${msg}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistTotalsChart extends BaseChart {
|
||||||
|
constructor(id, options) {
|
||||||
|
super(id, { ...options, chartType: "bar" });
|
||||||
|
}
|
||||||
|
|
||||||
|
get datasets() {
|
||||||
|
return [{
|
||||||
|
data: this.options.series,
|
||||||
|
backgroundColor: this.colors[0],
|
||||||
|
borderWidth: 0,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
get chartOptions() {
|
||||||
|
return {
|
||||||
|
aspectRatio: 6,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
text: "Total jobs",
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: "x",
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (item) => `${item.parsed.y} jobs`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistBubbleChart extends BaseChart {
|
||||||
|
constructor(id, options) {
|
||||||
|
super(id, { ...options, chartType: "bubble" });
|
||||||
|
|
||||||
|
this.addMarksToChart();
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get datasets() {
|
||||||
|
const data = [];
|
||||||
|
let maxCount = 0;
|
||||||
|
|
||||||
|
Object.entries(this.options.hist).forEach(([bucket, hist]) => {
|
||||||
|
hist.forEach((count, histBucket) => {
|
||||||
|
if (count > 0) {
|
||||||
|
data.push({
|
||||||
|
x: bucket,
|
||||||
|
// histogram data is ordered fastest to slowest, but this.histIntervals is
|
||||||
|
// slowest to fastest (so it displays correctly on the chart).
|
||||||
|
y:
|
||||||
|
this.options.histIntervals[this.options.histIntervals.length - 1 - histBucket] /
|
||||||
|
1000,
|
||||||
|
count: count,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count > maxCount) maxCount = count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chart.js will not calculate the bubble size. We have to do that.
|
||||||
|
const maxRadius = this.ctx.offsetWidth / this.options.labels.length;
|
||||||
|
const minRadius = 1
|
||||||
|
const multiplier = (maxRadius / maxCount) * 1.5;
|
||||||
|
data.forEach((entry) => {
|
||||||
|
entry.r = entry.count * multiplier + minRadius;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [{
|
||||||
|
data: data,
|
||||||
|
backgroundColor: "#537bc4",
|
||||||
|
borderColor: "#537bc4",
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
get chartOptions() {
|
||||||
|
return {
|
||||||
|
aspectRatio: 3,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: "category",
|
||||||
|
labels: this.options.labels,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
text: "Execution time (sec)",
|
||||||
|
display: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: "x",
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: (items) => `${items[0].raw.x} UTC`,
|
||||||
|
label: (item) =>
|
||||||
|
`${item.parsed.y} seconds: ${item.raw.count} job${
|
||||||
|
item.raw.count == 1 ? "" : "s"
|
||||||
|
}`,
|
||||||
|
footer: (items) => {
|
||||||
|
const bucket = items[0].raw.x;
|
||||||
|
const marks = this.options.marks.filter(([b, _]) => b == bucket);
|
||||||
return marks.map(([b, msg]) => `Deploy: ${msg}`);
|
return marks.map(([b, msg]) => `Deploy: ${msg}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -67,10 +67,15 @@ body {
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h1, h2, h3 {
|
||||||
|
font-size: 24px;
|
||||||
line-height: 45px;
|
line-height: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-with-subheader h2 {
|
||||||
|
margin-top: -18px;
|
||||||
|
}
|
||||||
|
|
||||||
.centered {
|
.centered {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -988,3 +993,7 @@ div.interval-slider input {
|
||||||
outline: 1px solid #888;
|
outline: 1px solid #888;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
margin: 20px 0 30px;
|
||||||
|
}
|
||||||
|
|
|
@ -89,3 +89,5 @@ en: # <---- change this to your locale code
|
||||||
ExecutionTime: Total Execution Time
|
ExecutionTime: Total Execution Time
|
||||||
AvgExecutionTime: Average Execution Time
|
AvgExecutionTime: Average Execution Time
|
||||||
Context: Context
|
Context: Context
|
||||||
|
Bucket: Bucket
|
||||||
|
NoJobMetricsFound: No recent job metrics were found
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<script type="text/javascript" src="<%= root_path %>javascripts/chartjs-plugin-annotation.min.js"></script>
|
<script type="text/javascript" src="<%= root_path %>javascripts/chartjs-plugin-annotation.min.js"></script>
|
||||||
<script type="text/javascript" src="<%= root_path %>javascripts/metrics.js"></script>
|
<script type="text/javascript" src="<%= root_path %>javascripts/metrics.js"></script>
|
||||||
|
|
||||||
<h3><%= t('Metrics') %></h3>
|
<h2>Total execution time</h2>
|
||||||
|
|
||||||
<%
|
<%
|
||||||
table_limit = 20
|
table_limit = 20
|
||||||
|
@ -11,11 +11,11 @@
|
||||||
visible_kls = job_results.first(chart_limit).map(&:first)
|
visible_kls = job_results.first(chart_limit).map(&:first)
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<canvas id="metrics-chart"></canvas>
|
<canvas id="job-metrics-overview-chart"></canvas>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.metricsChart = new MetricsChart(
|
window.jobMetricsChart = new JobMetricsOverviewChart(
|
||||||
"metrics-chart",
|
"job-metrics-overview-chart",
|
||||||
<%= Sidekiq.dump_json({
|
<%= Sidekiq.dump_json({
|
||||||
series: job_results.map { |(kls, jr)| [kls, jr.dig("series", "s")] }.to_h,
|
series: job_results.map { |(kls, jr)| [kls, jr.dig("series", "s")] }.to_h,
|
||||||
marks: @query_result.marks.map { |m| [m.bucket, m.label] },
|
marks: @query_result.marks.map { |m| [m.bucket, m.label] },
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h3>Most Time-Consuming Jobs</h3>
|
<h2>Most Time-Consuming Jobs</h2>
|
||||||
|
|
||||||
<div class="table_container">
|
<div class="table_container">
|
||||||
<table class="table table-bordered table-striped table-hover">
|
<table class="table table-bordered table-striped table-hover">
|
||||||
|
@ -51,12 +51,12 @@
|
||||||
/>
|
/>
|
||||||
<code><a href="<%= root_path %>metrics/<%= kls %>"><%= kls %></a></code>
|
<code><a href="<%= root_path %>metrics/<%= kls %>"><%= kls %></a></code>
|
||||||
</div>
|
</div>
|
||||||
<script>metricsChart.registerSwatch("<%= id %>")</script>
|
<script>jobMetricsChart.registerSwatch("<%= id %>")</script>
|
||||||
</td>
|
</td>
|
||||||
<td><%= jr.dig("totals", "p") %></td>
|
<td><%= jr.dig("totals", "p") %></td>
|
||||||
<td><%= jr.dig("totals", "f") %></td>
|
<td><%= jr.dig("totals", "f") %></td>
|
||||||
<td><%= jr.dig("totals", "s").round(0) %> seconds</td>
|
<td><%= jr.dig("totals", "s").round(2) %> seconds</td>
|
||||||
<td><%= (jr.dig("totals", "s") / jr.dig("totals", "p")).round(2) %> seconds</td>
|
<td><%= jr.total_avg("s").round(2) %> seconds</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
@ -66,4 +66,4 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><small>Data from <%= @query_result.starts_at %> to <%= @query_result.ends_at %></small></p>
|
<p><small>Data from <%= @query_result.starts_at %> to <%= @query_result.ends_at %></small></p>
|
||||||
|
|
|
@ -1,92 +1,88 @@
|
||||||
|
<script type="text/javascript" src="<%= root_path %>javascripts/chart.min.js"></script>
|
||||||
|
<script type="text/javascript" src="<%= root_path %>javascripts/chartjs-plugin-annotation.min.js"></script>
|
||||||
|
<script type="text/javascript" src="<%= root_path %>javascripts/metrics.js"></script>
|
||||||
|
|
||||||
<h2><%= t('Metrics') %> / <%= h @name %></h2>
|
<%
|
||||||
|
job_result = @query_result.job_results[@name]
|
||||||
|
hist_totals = job_result.hist.values.first.zip(*job_result.hist.values[1..-1]).map(&:sum)
|
||||||
|
bucket_labels =Sidekiq::Metrics::Histogram::LABELS
|
||||||
|
bucket_intervals =Sidekiq::Metrics::Histogram::BUCKET_INTERVALS.reverse
|
||||||
|
|
||||||
<div class="row chart">
|
# Replace INFINITY since it can't be represented as JSON
|
||||||
<div id="realtime" data-processed-label="<%= t('Processed') %>" data-failed-label="<%= t('Failed') %>"></div>
|
bucket_intervals[0] = bucket_intervals[1] * 2
|
||||||
</div>
|
%>
|
||||||
|
|
||||||
<% data = @resultset[:data] %>
|
<% if job_result.totals["s"] > 0 %>
|
||||||
<div class="table_container">
|
<div class="header-with-subheader">
|
||||||
<table class="table table-bordered table-striped table-hover">
|
<h1>
|
||||||
<tbody>
|
<a href="<%= root_path %>/metrics"><%= t(:metrics).to_s.titleize %></a> /
|
||||||
<tr>
|
<%= h @name %>
|
||||||
<th><%= t('Time') %></th>
|
</h1>
|
||||||
<th><%= t('Processed') %></th>
|
<h2>Histogram summary</h2>
|
||||||
<th><%= t('ExecutionTime') %></th>
|
</div>
|
||||||
<th><%= t('Failed') %></th>
|
|
||||||
<th><%= t('Deploy') %></th>
|
|
||||||
<th><%= t('Histogram') %></th>
|
|
||||||
</tr>
|
|
||||||
<% data.each do |hash| %>
|
|
||||||
<tr><td><%= hash[:time] %></td><td><%= hash[:p] %></td><td><%= hash[:ms] %></td><td><%= hash[:f] %></td><td><%= hash[:mark] %></td><td><%= hash[:hist] %></td></tr>
|
|
||||||
<% end %>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
<canvas id="hist-totals-chart"></canvas>
|
||||||
Data from <%= @resultset[:starts_at] %> to <%= @resultset[:ends_at] %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<% atad = data.reverse %>
|
<script>
|
||||||
<script type="text/javascript" src="<%= root_path %>javascripts/graph.js"></script>
|
window.histTotalsChart = new HistTotalsChart(
|
||||||
<script>
|
"hist-totals-chart",
|
||||||
var palette = new Rickshaw.Color.Palette();
|
<%= Sidekiq.dump_json({
|
||||||
var data = [ {
|
series: hist_totals,
|
||||||
name: "Processed",
|
labels: bucket_labels,
|
||||||
color: palette.color(),
|
}) %>
|
||||||
data: [ <% atad.each do |hash| %>
|
)
|
||||||
{ x: <%= hash[:epoch] %>, y: <%= hash[:p] %> },
|
</script>
|
||||||
<% end %> ]
|
|
||||||
}, {
|
|
||||||
name: "Failed",
|
|
||||||
color: palette.color(),
|
|
||||||
data: [ <% atad.each do |hash| %>
|
|
||||||
{ x: <%= hash[:epoch] %>, y: <%= hash[:f] %> },
|
|
||||||
<% end %>
|
|
||||||
]
|
|
||||||
}, {
|
|
||||||
name: "Execution Time",
|
|
||||||
color: palette.color(),
|
|
||||||
data: [ <% atad.each do |hash| %>
|
|
||||||
{ x: <%= hash[:epoch] %>, y: <%= hash[:ms] %> },
|
|
||||||
<% end %>
|
|
||||||
]
|
|
||||||
} ];
|
|
||||||
|
|
||||||
// TODO What to do with this? Minutely hover detail with a histogram bar chart?
|
<h2>Performance over time</h2>
|
||||||
var histogramData = [ <% atad.each do |hash| %>
|
|
||||||
{ x: <%= hash[:epoch] %>, hist: <%= hash[:hist] %> },
|
|
||||||
<% end %> ]
|
|
||||||
var histogramLabels = <%= Sidekiq::Metrics::Histogram::LABELS.inspect %>;
|
|
||||||
|
|
||||||
var timeInterval = 60000;
|
<canvas id="hist-bubble-chart"></canvas>
|
||||||
var graphElement = document.getElementById("realtime");
|
|
||||||
|
|
||||||
var graph = new Rickshaw.Graph({
|
<script>
|
||||||
element: graphElement,
|
window.histBubbleChart = new HistBubbleChart(
|
||||||
width: responsiveWidth(),
|
"hist-bubble-chart",
|
||||||
renderer: 'line',
|
<%= Sidekiq.dump_json({
|
||||||
interpolation: 'linear',
|
hist: job_result.hist,
|
||||||
series: data,
|
marks: @query_result.marks.map { |m| [m.bucket, m.label] },
|
||||||
});
|
labels: @query_result.buckets,
|
||||||
var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } );
|
histIntervals: bucket_intervals,
|
||||||
|
}) %>
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
var y_axis = new Rickshaw.Graph.Axis.Y( {
|
<div class="table_container">
|
||||||
graph: graph,
|
<table class="table table-bordered table-striped table-hover">
|
||||||
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
|
<tbody>
|
||||||
ticksTreatment: 'glow'
|
<tr>
|
||||||
});
|
<th><%= t('Time') %></th>
|
||||||
|
<th><%= t('Processed') %></th>
|
||||||
|
<th><%= t('Failed') %></th>
|
||||||
|
<th><%= t('ExecutionTime') %></th>
|
||||||
|
<th><%= t('AvgExecutionTime') %></th>
|
||||||
|
</tr>
|
||||||
|
<% @query_result.buckets.reverse.each do |bucket| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= bucket %></td>
|
||||||
|
<td><%= job_result.series.dig("p", bucket) %></td>
|
||||||
|
<td><%= job_result.series.dig("f", bucket) %></td>
|
||||||
|
<% if (total_sec = job_result.series.dig("s", bucket)) > 0 %>
|
||||||
|
<td><%= total_sec.round(2) %> seconds</td>
|
||||||
|
<td><%= job_result.series_avg("s")[bucket].round(2) %> seconds</td>
|
||||||
|
<% else %>
|
||||||
|
<td>—</td>
|
||||||
|
<td>—</td>
|
||||||
|
<% end %>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<h1>
|
||||||
|
<a href="<%= root_path %>/metrics"><%= t(:metrics).to_s.titleize %></a> /
|
||||||
|
<%= h @name %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
graph.render();
|
<div class="alert alert-success"><%= t('NoJobMetricsFound') %></div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
var hoverDetail = new Rickshaw.Graph.HoverDetail( {
|
<p><small>Data from <%= @query_result.starts_at %> to <%= @query_result.ends_at %></small></p>
|
||||||
graph: graph,
|
|
||||||
// formatter: function(series, x, y) {
|
|
||||||
// var date = '<span class="date">' + new Date(x * 1000).toUTCString() + '</span>';
|
|
||||||
// var swatch = '<span class="detail_swatch" style="background-color: ' + series.color + '"></span>';
|
|
||||||
// var content = swatch + series.name + ": " + parseInt(y) + '<br>' + date;
|
|
||||||
// return content;
|
|
||||||
// }
|
|
||||||
} );
|
|
||||||
</script>
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue