forked from lix-project/hydra
Add support for tracking custom metrics
Builds can now emit metrics that Hydra will store in its database and render as time series via flot charts. Typical applications are to keep track of performance indicators, coverage percentages, artifact sizes, and so on. For example, a coverage build can emit the coverage percentage as follows: echo "lineCoverage $pct %" > $out/nix-support/hydra-metrics Graphs of all metrics for a job can be seen at http://.../job/<project>/<jobset>/<job>#tabs-charts Specific metrics are also visible at http://.../job/<project>/<jobset>/<job>/metric/<metric> The latter URL also allows getting the data in JSON format (e.g. via "curl -H 'Accept: application/json'").
This commit is contained in:
parent
8092149a9f
commit
4d26546d3c
|
@ -7,6 +7,21 @@
|
|||
using namespace nix;
|
||||
|
||||
|
||||
static std::tuple<bool, string> secureRead(Path fileName)
|
||||
{
|
||||
auto fail = std::make_tuple(false, "");
|
||||
|
||||
if (!pathExists(fileName)) return fail;
|
||||
|
||||
try {
|
||||
/* For security, resolve symlinks. */
|
||||
fileName = canonPath(fileName, true);
|
||||
if (!isInStore(fileName)) return fail;
|
||||
return std::make_tuple(true, readFile(fileName));
|
||||
} catch (Error & e) { return fail; }
|
||||
}
|
||||
|
||||
|
||||
BuildOutput getBuildOutput(std::shared_ptr<StoreAPI> store, const Derivation & drv)
|
||||
{
|
||||
BuildOutput res;
|
||||
|
@ -40,22 +55,12 @@ BuildOutput getBuildOutput(std::shared_ptr<StoreAPI> store, const Derivation & d
|
|||
Path failedFile = output + "/nix-support/failed";
|
||||
if (pathExists(failedFile)) res.failed = true;
|
||||
|
||||
Path productsFile = output + "/nix-support/hydra-build-products";
|
||||
if (!pathExists(productsFile)) continue;
|
||||
auto file = secureRead(output + "/nix-support/hydra-build-products");
|
||||
if (!std::get<0>(file)) continue;
|
||||
|
||||
explicitProducts = true;
|
||||
|
||||
/* For security, resolve symlinks. */
|
||||
try {
|
||||
productsFile = canonPath(productsFile, true);
|
||||
} catch (Error & e) { continue; }
|
||||
if (!isInStore(productsFile)) continue;
|
||||
|
||||
string contents;
|
||||
try {
|
||||
contents = readFile(productsFile);
|
||||
} catch (Error & e) { continue; }
|
||||
|
||||
for (auto & line : tokenizeString<Strings>(contents, "\n")) {
|
||||
for (auto & line : tokenizeString<Strings>(std::get<1>(file), "\n")) {
|
||||
BuildProduct product;
|
||||
|
||||
Regex::Subs subs;
|
||||
|
@ -122,5 +127,19 @@ BuildOutput getBuildOutput(std::shared_ptr<StoreAPI> store, const Derivation & d
|
|||
// FIXME: validate release name
|
||||
}
|
||||
|
||||
/* Get metrics. */
|
||||
for (auto & output : outputs) {
|
||||
auto file = secureRead(output + "/nix-support/hydra-metrics");
|
||||
for (auto & line : tokenizeString<Strings>(std::get<1>(file), "\n")) {
|
||||
auto fields = tokenizeString<std::vector<std::string>>(line);
|
||||
if (fields.size() < 2) continue;
|
||||
BuildMetric metric;
|
||||
metric.name = fields[0]; // FIXME: validate
|
||||
metric.value = atof(fields[1].c_str()); // FIXME
|
||||
metric.unit = fields.size() >= 3 ? fields[2] : "";
|
||||
res.metrics[metric.name] = metric;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,12 @@ struct BuildProduct
|
|||
BuildProduct() { }
|
||||
};
|
||||
|
||||
struct BuildMetric
|
||||
{
|
||||
std::string name, unit;
|
||||
double value;
|
||||
};
|
||||
|
||||
struct BuildOutput
|
||||
{
|
||||
/* Whether this build has failed with output, i.e., the build
|
||||
|
@ -27,6 +33,8 @@ struct BuildOutput
|
|||
unsigned long long closureSize = 0, size = 0;
|
||||
|
||||
std::list<BuildProduct> products;
|
||||
|
||||
std::map<std::string, BuildMetric> metrics;
|
||||
};
|
||||
|
||||
BuildOutput getBuildOutput(std::shared_ptr<nix::StoreAPI> store, const nix::Derivation & drv);
|
||||
|
|
|
@ -238,6 +238,19 @@ void State::markSucceededBuild(pqxx::work & txn, Build::ptr build,
|
|||
(product.defaultPath).exec();
|
||||
}
|
||||
|
||||
for (auto & metric : res.metrics) {
|
||||
txn.parameterized
|
||||
("insert into BuildMetrics (build, name, unit, value, project, jobset, job, timestamp) values ($1, $2, $3, $4, $5, $6, $7, $8)")
|
||||
(build->id)
|
||||
(metric.second.name)
|
||||
(metric.second.unit, metric.second.unit != "")
|
||||
(metric.second.value)
|
||||
(build->projectName)
|
||||
(build->jobsetName)
|
||||
(build->jobName)
|
||||
(build->timestamp).exec();
|
||||
}
|
||||
|
||||
nrBuildsDone++;
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ void State::getQueuedBuilds(Connection & conn, std::shared_ptr<StoreAPI> store,
|
|||
{
|
||||
pqxx::work txn(conn);
|
||||
|
||||
auto res = txn.parameterized("select id, project, jobset, job, drvPath, maxsilent, timeout from Builds where id > $1 and finished = 0 order by id")(lastBuildId).exec();
|
||||
auto res = txn.parameterized("select id, project, jobset, job, drvPath, maxsilent, timeout, timestamp from Builds where id > $1 and finished = 0 order by id")(lastBuildId).exec();
|
||||
|
||||
for (auto const & row : res) {
|
||||
auto builds_(builds.lock());
|
||||
|
@ -76,9 +76,12 @@ void State::getQueuedBuilds(Connection & conn, std::shared_ptr<StoreAPI> store,
|
|||
auto build = std::make_shared<Build>();
|
||||
build->id = id;
|
||||
build->drvPath = row["drvPath"].as<string>();
|
||||
build->fullJobName = row["project"].as<string>() + ":" + row["jobset"].as<string>() + ":" + row["job"].as<string>();
|
||||
build->projectName = row["project"].as<string>();
|
||||
build->jobsetName = row["jobset"].as<string>();
|
||||
build->jobName = row["job"].as<string>();
|
||||
build->maxSilentTime = row["maxsilent"].as<int>();
|
||||
build->buildTimeout = row["timeout"].as<int>();
|
||||
build->timestamp = row["timestamp"].as<time_t>();
|
||||
|
||||
newBuilds.emplace(std::make_pair(build->drvPath, build));
|
||||
}
|
||||
|
@ -89,7 +92,7 @@ void State::getQueuedBuilds(Connection & conn, std::shared_ptr<StoreAPI> store,
|
|||
std::function<void(Build::ptr)> createBuild;
|
||||
|
||||
createBuild = [&](Build::ptr build) {
|
||||
printMsg(lvlTalkative, format("loading build %1% (%2%)") % build->id % build->fullJobName);
|
||||
printMsg(lvlTalkative, format("loading build %1% (%2%)") % build->id % build->fullJobName());
|
||||
nrAdded++;
|
||||
|
||||
if (!store->isValidPath(build->drvPath)) {
|
||||
|
|
|
@ -38,6 +38,7 @@ typedef enum {
|
|||
bssFailed = 1,
|
||||
bssAborted = 4,
|
||||
bssTimedOut = 7,
|
||||
bssCachedFailure = 8,
|
||||
bssUnsupported = 9,
|
||||
bssBusy = 100, // not stored
|
||||
} BuildStepStatus;
|
||||
|
@ -67,12 +68,18 @@ struct Build
|
|||
BuildID id;
|
||||
nix::Path drvPath;
|
||||
std::map<std::string, nix::Path> outputs;
|
||||
std::string fullJobName;
|
||||
std::string projectName, jobsetName, jobName;
|
||||
time_t timestamp;
|
||||
unsigned int maxSilentTime, buildTimeout;
|
||||
|
||||
std::shared_ptr<Step> toplevel;
|
||||
|
||||
std::atomic_bool finishedInDB{false};
|
||||
|
||||
std::string fullJobName()
|
||||
{
|
||||
return projectName + ":" + jobsetName + ":" + jobName;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -77,6 +77,9 @@ sub overview : Chained('job') PathPart('') Args(0) {
|
|||
, jobset => $c->stash->{jobset}->name
|
||||
, job => $c->stash->{job}->name
|
||||
})->count == 1 if $c->user_exists;
|
||||
|
||||
$c->stash->{metrics} = [ $job->buildmetrics->search(
|
||||
{ }, { select => ["name"], distinct => 1, order_by => "timestamp desc", }) ];
|
||||
}
|
||||
|
||||
|
||||
|
@ -110,6 +113,20 @@ sub output_sizes : Chained('job') PathPart('output-sizes') Args(0) {
|
|||
}
|
||||
|
||||
|
||||
sub metric : Chained('job') PathPart('metric') Args(1) {
|
||||
my ($self, $c, $metricName) = @_;
|
||||
|
||||
$c->stash->{template} = 'metric.tt';
|
||||
$c->stash->{metricName} = $metricName;
|
||||
|
||||
my @res = $c->stash->{job}->buildmetrics->search(
|
||||
{ name => $metricName },
|
||||
{ order_by => "timestamp", columns => [ "build", "name", "timestamp", "value", "unit" ] });
|
||||
|
||||
$self->status_ok($c, entity => [ map { { id => $_->get_column("build"), timestamp => $_ ->timestamp, value => $_->value, unit => $_->unit } } @res ]);
|
||||
}
|
||||
|
||||
|
||||
# Hydra::Base::Controller::ListBuilds needs this.
|
||||
sub get_builds : Chained('job') PathPart('') CaptureArgs(0) {
|
||||
my ($self, $c) = @_;
|
||||
|
|
187
src/lib/Hydra/Schema/BuildMetrics.pm
Normal file
187
src/lib/Hydra/Schema/BuildMetrics.pm
Normal file
|
@ -0,0 +1,187 @@
|
|||
use utf8;
|
||||
package Hydra::Schema::BuildMetrics;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Hydra::Schema::BuildMetrics
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 COMPONENTS LOADED
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L<Hydra::Component::ToJSON>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->load_components("+Hydra::Component::ToJSON");
|
||||
|
||||
=head1 TABLE: C<BuildMetrics>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("BuildMetrics");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 build
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 name
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 0
|
||||
|
||||
=head2 unit
|
||||
|
||||
data_type: 'text'
|
||||
is_nullable: 1
|
||||
|
||||
=head2 value
|
||||
|
||||
data_type: 'double precision'
|
||||
is_nullable: 0
|
||||
|
||||
=head2 project
|
||||
|
||||
data_type: 'text'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 jobset
|
||||
|
||||
data_type: 'text'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 job
|
||||
|
||||
data_type: 'text'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 timestamp
|
||||
|
||||
data_type: 'integer'
|
||||
is_nullable: 0
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"build",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
"name",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"unit",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"value",
|
||||
{ data_type => "double precision", is_nullable => 0 },
|
||||
"project",
|
||||
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
|
||||
"jobset",
|
||||
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
|
||||
"job",
|
||||
{ data_type => "text", is_foreign_key => 1, is_nullable => 0 },
|
||||
"timestamp",
|
||||
{ data_type => "integer", is_nullable => 0 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</build>
|
||||
|
||||
=item * L</name>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("build", "name");
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 build
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<Hydra::Schema::Builds>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"build",
|
||||
"Hydra::Schema::Builds",
|
||||
{ id => "build" },
|
||||
{ is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
|
||||
);
|
||||
|
||||
=head2 job
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<Hydra::Schema::Jobs>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"job",
|
||||
"Hydra::Schema::Jobs",
|
||||
{ jobset => "jobset", name => "job", project => "project" },
|
||||
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "CASCADE" },
|
||||
);
|
||||
|
||||
=head2 jobset
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<Hydra::Schema::Jobsets>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"jobset",
|
||||
"Hydra::Schema::Jobsets",
|
||||
{ name => "jobset", project => "project" },
|
||||
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "CASCADE" },
|
||||
);
|
||||
|
||||
=head2 project
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<Hydra::Schema::Projects>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"project",
|
||||
"Hydra::Schema::Projects",
|
||||
{ name => "project" },
|
||||
{ is_deferrable => 0, on_delete => "NO ACTION", on_update => "CASCADE" },
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qoPm5/le+sVHigW4Dmum2Q
|
||||
|
||||
sub json_hint {
|
||||
return { columns => ['value', 'unit'] };
|
||||
}
|
||||
|
||||
1;
|
|
@ -341,6 +341,21 @@ __PACKAGE__->has_many(
|
|||
undef,
|
||||
);
|
||||
|
||||
=head2 buildmetrics
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<Hydra::Schema::BuildMetrics>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"buildmetrics",
|
||||
"Hydra::Schema::BuildMetrics",
|
||||
{ "foreign.build" => "self.id" },
|
||||
undef,
|
||||
);
|
||||
|
||||
=head2 buildoutputs
|
||||
|
||||
Type: has_many
|
||||
|
@ -535,8 +550,8 @@ __PACKAGE__->many_to_many(
|
|||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:03:55
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:EwxiaQpqbdzI9RvU0uUtLQ
|
||||
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Y2lDtgY8EBLOuCHAI8fWRQ
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"dependents",
|
||||
|
@ -630,6 +645,7 @@ my %hint = (
|
|||
buildoutputs => 'name',
|
||||
buildinputs_builds => 'name',
|
||||
buildproducts => 'productnr',
|
||||
buildmetrics => 'name',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -81,6 +81,25 @@ __PACKAGE__->set_primary_key("project", "jobset", "name");
|
|||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 buildmetrics
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<Hydra::Schema::BuildMetrics>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"buildmetrics",
|
||||
"Hydra::Schema::BuildMetrics",
|
||||
{
|
||||
"foreign.job" => "self.name",
|
||||
"foreign.jobset" => "self.jobset",
|
||||
"foreign.project" => "self.project",
|
||||
},
|
||||
undef,
|
||||
);
|
||||
|
||||
=head2 builds
|
||||
|
||||
Type: has_many
|
||||
|
@ -150,7 +169,7 @@ __PACKAGE__->has_many(
|
|||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-09-29 19:41:42
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:lnZSd0gDXgLk8WQeAFqByA
|
||||
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vDAo9bzLca+QWfhOb9OLMg
|
||||
|
||||
1;
|
||||
|
|
|
@ -184,6 +184,24 @@ __PACKAGE__->set_primary_key("project", "name");
|
|||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 buildmetrics
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<Hydra::Schema::BuildMetrics>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"buildmetrics",
|
||||
"Hydra::Schema::BuildMetrics",
|
||||
{
|
||||
"foreign.jobset" => "self.name",
|
||||
"foreign.project" => "self.project",
|
||||
},
|
||||
undef,
|
||||
);
|
||||
|
||||
=head2 builds
|
||||
|
||||
Type: has_many
|
||||
|
@ -320,8 +338,8 @@ __PACKAGE__->has_many(
|
|||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-04-23 23:13:51
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CO0aE+jrjB+UrwGRzWZLlw
|
||||
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Coci9FdBAvUO9T3st2NEqA
|
||||
|
||||
my %hint = (
|
||||
columns => [
|
||||
|
|
|
@ -106,6 +106,21 @@ __PACKAGE__->set_primary_key("name");
|
|||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 buildmetrics
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<Hydra::Schema::BuildMetrics>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"buildmetrics",
|
||||
"Hydra::Schema::BuildMetrics",
|
||||
{ "foreign.project" => "self.name" },
|
||||
undef,
|
||||
);
|
||||
|
||||
=head2 builds
|
||||
|
||||
Type: has_many
|
||||
|
@ -267,8 +282,8 @@ Composing rels: L</projectmembers> -> username
|
|||
__PACKAGE__->many_to_many("usernames", "projectmembers", "username");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-04-23 23:13:08
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:fkd9ruEoVSBGIktmAj4u4g
|
||||
# Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:67kWIE0IGmEJTvOIATAKaw
|
||||
|
||||
my %hint = (
|
||||
columns => [
|
||||
|
|
|
@ -385,6 +385,25 @@
|
|||
</tr>
|
||||
[% END %]
|
||||
</table>
|
||||
|
||||
[% IF build.finished && build.buildmetrics %]
|
||||
<h3>Metrics</h3>
|
||||
|
||||
<table class="table table-small table-striped table-hover clickable-rows">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
[% FOREACH metric IN build.buildmetrics %]
|
||||
<tr>
|
||||
<td><tt><a class="row-link" href="[% c.uri_for('/job' project.name jobset.name job.name 'metric' metric.name) %]">[%HTML.escape(metric.name)%]</a></tt></td>
|
||||
<td>[%metric.value%][%metric.unit%]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
[% END %]
|
||||
|
||||
</div>
|
||||
|
||||
<div id="tabs-buildinputs" class="tab-pane">
|
||||
|
|
|
@ -566,12 +566,14 @@ BLOCK createChart %]
|
|||
success: function(data) {
|
||||
var ids = [];
|
||||
var d = [];
|
||||
var max = 0;
|
||||
var maxTime = 0;
|
||||
var minTime = Number.MAX_SAFE_INTEGER;
|
||||
data.forEach(function(x) {
|
||||
var t = x.timestamp * 1000;
|
||||
ids[t] = x.id;
|
||||
d.push([t, x.value [% IF yaxis == "mib" %] / (1024.0 * 1024.0)[% END %]]);
|
||||
max = Math.max(t, max);
|
||||
maxTime = Math.max(t, maxTime);
|
||||
minTime = Math.min(t, minTime);
|
||||
});
|
||||
|
||||
var options = {
|
||||
|
@ -634,7 +636,7 @@ BLOCK createChart %]
|
|||
});
|
||||
|
||||
// Zoom in to the last two months by default.
|
||||
plot.setSelection({ xaxis: { from: max - 60 * 24 * 60 * 60 * 1000, to: max } });
|
||||
plot.setSelection({ xaxis: { from: Math.max(minTime, maxTime - 60 * 24 * 60 * 60 * 1000), to: maxTime } });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -98,6 +98,14 @@ removed or had an evaluation error.</div>
|
|||
|
||||
[% INCLUDE createChart id="output-size" yaxis="mib" dataUrl=c.uri_for('/job' project.name jobset.name job.name 'output-sizes') %]
|
||||
|
||||
[% FOREACH metric IN metrics %]
|
||||
|
||||
<h3>Metric: <tt>[%HTML.escape(metric.name)%]</tt></h3>
|
||||
|
||||
[% INCLUDE createChart id="metric-${metric.name}" dataUrl=c.uri_for('/job' project.name jobset.name job.name 'metric' metric.name) %]
|
||||
|
||||
[% END %]
|
||||
|
||||
</div>
|
||||
|
||||
<div id="tabs-links" class="tab-pane">
|
||||
|
|
7
src/root/metric.tt
Normal file
7
src/root/metric.tt
Normal file
|
@ -0,0 +1,7 @@
|
|||
[% WRAPPER layout.tt title="Job metric ‘$metricName’" %]
|
||||
[% PROCESS common.tt %]
|
||||
|
||||
[% INCLUDE includeFlot %]
|
||||
[% INCLUDE createChart id="chart" dataUrl=c.req.uri %]
|
||||
|
||||
[% END %]
|
|
@ -29,6 +29,10 @@ table.productList {
|
|||
border-spacing: 0em 1em;
|
||||
}
|
||||
|
||||
table.table-small {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
span:target > span.dep-tree-line {
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -324,6 +324,28 @@ create table BuildProducts (
|
|||
);
|
||||
|
||||
|
||||
create table BuildMetrics (
|
||||
build integer not null,
|
||||
name text not null,
|
||||
|
||||
unit text,
|
||||
value double precision not null,
|
||||
|
||||
-- Denormalisation for performance: copy some columns from the
|
||||
-- corresponding build.
|
||||
project text not null,
|
||||
jobset text not null,
|
||||
job text not null,
|
||||
timestamp integer not null,
|
||||
|
||||
primary key (build, name),
|
||||
foreign key (build) references Builds(id) on delete cascade,
|
||||
foreign key (project) references Projects(name) on update cascade,
|
||||
foreign key (project, jobset) references Jobsets(project, name) on update cascade,
|
||||
foreign key (project, jobset, job) references Jobs(project, jobset, name) on update cascade
|
||||
);
|
||||
|
||||
|
||||
-- Cache for inputs of type "path" (used for testing Hydra), storing
|
||||
-- the SHA-256 hash and store path for each source path. Also stores
|
||||
-- the timestamp when we first saw the path have these contents, which
|
||||
|
@ -590,6 +612,7 @@ create trigger NrBuildsFinished after insert or update or delete on Builds
|
|||
|
||||
create index IndexBuildInputsOnBuild on BuildInputs(build);
|
||||
create index IndexBuildInputsOnDependency on BuildInputs(dependency);
|
||||
create index IndexBuildMetricsOnJobTimestamp on BuildMetrics(project, jobset, job, timestamp desc);
|
||||
create index IndexBuildProducstOnBuildAndType on BuildProducts(build, type);
|
||||
create index IndexBuildProductsOnBuild on BuildProducts(build);
|
||||
create index IndexBuildStepsOnBusy on BuildSteps(busy) where busy = 1;
|
||||
|
|
22
src/sql/upgrade-39.sql
Normal file
22
src/sql/upgrade-39.sql
Normal file
|
@ -0,0 +1,22 @@
|
|||
create table BuildMetrics (
|
||||
build integer not null,
|
||||
name text not null,
|
||||
|
||||
unit text,
|
||||
value double precision not null,
|
||||
|
||||
-- Denormalisation for performance: copy some columns from the
|
||||
-- corresponding build.
|
||||
project text not null,
|
||||
jobset text not null,
|
||||
job text not null,
|
||||
timestamp integer not null,
|
||||
|
||||
primary key (build, name),
|
||||
foreign key (build) references Builds(id) on delete cascade,
|
||||
foreign key (project) references Projects(name) on update cascade,
|
||||
foreign key (project, jobset) references Jobsets(project, name) on update cascade,
|
||||
foreign key (project, jobset, job) references Jobs(project, jobset, name) on update cascade
|
||||
);
|
||||
|
||||
create index IndexBuildMetricsOnJobTimestamp on BuildMetrics(project, jobset, job, timestamp desc);
|
Loading…
Reference in a new issue