1
0
Fork 0
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:
Adam McCrea 2022-08-16 13:56:36 -04:00 committed by GitHub
parent ff7e41924d
commit 0b3751bf29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 343 additions and 177 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}`);
}, },
}, },

View file

@ -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;
}

View file

@ -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

View file

@ -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>

View file

@ -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>&mdash;</td>
<td>&mdash;</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>