Add support for tracking custom metrics

[?]
Jul 30, 2015, 10:57 PM
T5BIOVJEMBIASP7EKQVV2N3VD6I56UXH6LCD5I33BDQEVHJAMGKQC

Dependencies

  • [2] LKEX7GZ4 Show buildinput and buildproduct information in the Builds API
  • [3] PCD3ZH6Z Partially revert 1c20cfdf2403feb78cef515faf15c04d5c9f17bd
  • [4] BB2KXLXZ Move the build time chart to the job page
  • [5] WHULPA6S Handle failure with output
  • [6] YHP5DSOO Improve parsing of hydra-build-products
  • [7] OG3Z3QGC Namespace cleanup
  • [8] MHVIT4JY Split hydra-queue-runner.cc more
  • [9] 4S5JF5JP Use latest DBIx::Class::Schema::Loader
  • [10] B72GLND4
  • [11] S5PV6IIM * Represent jobs explicitly in the DB.
  • [12] JTHWA6AM Rename aggregate members to constituents
  • [13] BHZXGT2H * Channels: provide an index page that lists all the packages in the
  • [14] D7PL2VWU Move more actions from the top bar
  • [15] K3HODXGH Check all inputs for blame but only email selected inputs
  • [16] 7ECJWNVX Cleanup Project model
  • [17] L2E6EVE2 * Merged the Build and Job tables.
  • [18] TX7Q4RAS Add page showing latest build steps
  • [19] CLJQCY2X * Store info about all the build actions and allow them to be
  • [20] CMU3YKOU * Store the release name.
  • [21] TWVSALRL * Allow the maximum number of concurrent builds per platform to be
  • [22] 2GUAKGTB Fix indentation of build.tt
  • [23] QLOLZHRX Allow a per-jobset check interval
  • [24] 37R34XJO * Negative caching: don't perform a build if a dependency already
  • [25] TQKGQ5R3
  • [26] FTPCV25M Store aggregate members in the database
  • [27] GEADFVZ5 hydra-queue-runner: Improved scheduling
  • [28] PMNWRTGJ Add multiple output support
  • [29] KSBB33RE Add a dashboard
  • [30] 4CELXP7P Remove the longDescription field
  • [31] ZI535LI6 * hydra: 'new' UI for project/jobset/job/build
  • [32] 6GZZDDQB Move the store path size chart to the job page
  • [33] RU7AQO7U * Role-based access control. Only admins can create projects. Only
  • [34] BD3GRK4B * Get rid of "positive failures" and separate log phases.
  • [35] PCKLFRT5 Support push notification of repository changes
  • [36] YTIDBFGU Drop unused "disabled" columns
  • [37] Y6AHH4TH Remove the logfile and logSize columns from the database
  • [38] QCGCX2BR Generalize lazy tabs
  • [39] S66BOMVU * Added authentication.
  • [40] FHF6IZJQ * Basic release management: releases are now dynamically computed as
  • [41] SJLEZFC4 check getHydraPath in stead of Envvar HYDRA_DBI directly
  • [42] GNIEG2GC * Disambiguate jobs by jobset name. I.e. jobs with the same name in
  • [43] YTZOC7C5 * Editing of jobset inputs.
  • [44] LCKWLQW3 * In Sqlite "release" is now a keyword, so use "release_" instead.
  • [45] R5D7DZPE
  • [46] YU6CND7C Remove support for views
  • [47] TLZ2SPBR
  • [48] ZVTSOVHN * Support Subversion checkouts.
  • [49] EYNG4EL4 * Regenerate the bindings from a clean sqlite database.
  • [50] DH3KNBAV Merge remote-tracking branch 'upstream/who-broke-builds' into upstream-master
  • [51] YAPITGB3 * Boolean inputs.
  • [52] LZVO64YG Merge in the first bits of the API work
  • [53] BXHG3HYL When renaming a jobset, add a redirect from the old name
  • [54] HJOEIMLR Refactor
  • [55] SHBLLAVH * More global substitution.
  • [56] 3E6IP3R3 * Add the name of the jobset to ReleaseSetJobs, otherwise we can't
  • [57] H7CNGK4O * Log evaluation errors etc. in the DB.
  • [58] 6BLUKEQ2 * Caching of "path" inputs, and fake a revision number for those.
  • [59] SS4TZXNU Distinguish between permanent evaluation errors and transient input errors
  • [60] CVWVXXJU Scale the Y axis to the visible points
  • [61] AHTEIK7G * Added a maintainers field to the Builds table.
  • [62] V4RNHJNR * Add a link to each project's homepage. Suggested by karltk.
  • [63] VJHIHMEH * Store the meta.longDescription and meta.license attributes in the
  • [64] D3DIBMOK * For products that are directories (like manuals), allow a default
  • [65] ECBA3GQO * Make the schema class names match the case of the SQL table names.
  • [66] 3ZCEPLNO
  • [67] W5OAZWPD Drop the errorMsg column in the Jobs table
  • [68] G2T4WAHI Store the inputs of each evaluation in the database
  • [69] KQAQ4FIF Update Schema classes
  • [70] SB2V735V Keep track of the database schema version
  • [71] ZB3JV52W Add a "My jobsets" tab to the dashboard
  • [72] KOTB7BKV
  • [73] JIJDYWPY Remove the Build menu from the top bar
  • [74] IK53RV4V
  • [75] X27GNHDV * Basic job info in the database.
  • [76] 24BMQDZA Start of single-process hydra-queue-runner
  • [*] PQFOMNTL hydra-queue-runner: More stats
  • [*] DEMSSSB2 * Controller for jobs which inherits all actions in ListBuilds. So
  • [*] D5QIOJGP * Move everything up one directory.
  • [*] J5UVLXOK * Start of a basic Catalyst web interface.
  • [*] ZIIXICG7 Make the outputs' outpaths available via the Build JSON API
  • [*] JM3DPYOM generated schema with new dbix class schema loader, grrrrrr
  • [*] ZWCTAZGL added newsitems, added some admin options to clear various caches.
  • [*] ODNCGFQ5 * Improved the navigation bar: don't include all projects (since that
  • [*] XAJFR6SR Add a chart to the job pages showing the closure size over time
  • [*] HQ54SEMS Add more spacing between products
  • [*] N22GPKYT * Put info about logs / build products in the DB.
  • [*] 6QRHXIM3 * Speed up the jobset index page. Especially the query to get the

Change contents

  • edit in src/hydra-queue-runner/build-result.cc at line 8
    [10.553]
    [10.553]
    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; }
    }
  • replacement in src/hydra-queue-runner/build-result.cc at line 58
    [5.114][10.1182:1305](),[10.1182][10.1182:1305]()
    Path productsFile = output + "/nix-support/hydra-build-products";
    if (!pathExists(productsFile)) continue;
    [5.114]
    [10.1305]
    auto file = secureRead(output + "/nix-support/hydra-build-products");
    if (!std::get<0>(file)) continue;
  • replacement in src/hydra-queue-runner/build-result.cc at line 63
    [10.1339][10.1339:1385](),[10.1385][6.326:440](),[6.440][10.1439:1488](),[10.1439][10.1439:1488](),[10.1488][6.441:569](),[6.569][10.1524:1525](),[10.1524][10.1524:1525](),[10.1525][6.570:640]()
    /* 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")) {
    [10.1339]
    [6.640]
    for (auto & line : tokenizeString<Strings>(std::get<1>(file), "\n")) {
  • edit in src/hydra-queue-runner/build-result.cc at line 130
    [10.4269]
    [10.4269]
    /* 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;
    }
    }
  • edit in src/hydra-queue-runner/build-result.hh at line 16
    [10.4593]
    [10.4593]
    };
    struct BuildMetric
    {
    std::string name, unit;
    double value;
  • edit in src/hydra-queue-runner/build-result.hh at line 36
    [10.4737]
    [10.4737]
    std::map<std::string, BuildMetric> metrics;
  • edit in src/hydra-queue-runner/hydra-queue-runner.cc at line 241
    [10.19015]
    [78.410]
    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();
    }
  • replacement in src/hydra-queue-runner/queue-monitor.cc at line 67
    [8.21931][8.21931:22108]()
    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();
    [8.21931]
    [8.22108]
    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();
  • replacement in src/hydra-queue-runner/queue-monitor.cc at line 79
    [8.22529][8.22529:22658]()
    build->fullJobName = row["project"].as<string>() + ":" + row["jobset"].as<string>() + ":" + row["job"].as<string>();
    [8.22529]
    [8.22658]
    build->projectName = row["project"].as<string>();
    build->jobsetName = row["jobset"].as<string>();
    build->jobName = row["job"].as<string>();
  • edit in src/hydra-queue-runner/queue-monitor.cc at line 84
    [8.22781]
    [8.22781]
    build->timestamp = row["timestamp"].as<time_t>();
  • replacement in src/hydra-queue-runner/queue-monitor.cc at line 95
    [8.23024][8.23024:23124]()
    printMsg(lvlTalkative, format("loading build %1% (%2%)") % build->id % build->fullJobName);
    [8.23024]
    [8.23124]
    printMsg(lvlTalkative, format("loading build %1% (%2%)") % build->id % build->fullJobName());
  • edit in src/hydra-queue-runner/state.hh at line 41
    [10.1843]
    [10.1843]
    bssCachedFailure = 8,
  • replacement in src/hydra-queue-runner/state.hh at line 71
    [7.354][10.2121:2150](),[10.2121][10.2121:2150]()
    std::string fullJobName;
    [7.354]
    [10.2150]
    std::string projectName, jobsetName, jobName;
    time_t timestamp;
  • edit in src/hydra-queue-runner/state.hh at line 78
    [10.2276]
    [10.2276]
    std::string fullJobName()
    {
    return projectName + ":" + jobsetName + ":" + jobName;
    }
  • edit in src/lib/Hydra/Controller/Job.pm at line 80
    [10.228]
    [4.0]
    $c->stash->{metrics} = [ $job->buildmetrics->search(
    { }, { select => ["name"], distinct => 1, order_by => "timestamp desc", }) ];
  • edit in src/lib/Hydra/Controller/Job.pm at line 116
    [10.404]
    [79.548]
    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 ]);
    }
  • file addition: BuildMetrics.pm (----------)
    [80.477]
    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;
  • edit in src/lib/Hydra/Schema/Builds.pm at line 341
    [10.9620]
    [10.10120]
    undef,
    );
    =head2 buildmetrics
    Type: has_many
    Related object: L<Hydra::Schema::BuildMetrics>
    =cut
    __PACKAGE__->has_many(
    "buildmetrics",
    "Hydra::Schema::BuildMetrics",
    { "foreign.build" => "self.id" },
  • replacement in src/lib/Hydra/Schema/Builds.pm at line 553
    [10.3378][9.357:499]()
    # Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:03:55
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:EwxiaQpqbdzI9RvU0uUtLQ
    [10.3378]
    [10.4481]
    # Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Y2lDtgY8EBLOuCHAI8fWRQ
  • edit in src/lib/Hydra/Schema/Builds.pm at line 648
    [2.502]
    [82.158]
    buildmetrics => 'name',
  • edit in src/lib/Hydra/Schema/Jobs.pm at line 83
    [83.14424]
    [83.14424]
    =head2 buildmetrics
    Type: has_many
    Related object: L<Hydra::Schema::BuildMetrics>
    =cut
  • edit in src/lib/Hydra/Schema/Jobs.pm at line 92
    [83.14425]
    [10.15942]
    __PACKAGE__->has_many(
    "buildmetrics",
    "Hydra::Schema::BuildMetrics",
    {
    "foreign.job" => "self.name",
    "foreign.jobset" => "self.jobset",
    "foreign.project" => "self.project",
    },
    undef,
    );
  • replacement in src/lib/Hydra/Schema/Jobs.pm at line 172
    [10.1556][10.0:142]()
    # Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-09-29 19:41:42
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:lnZSd0gDXgLk8WQeAFqByA
    [10.1556]
    [10.2756]
    # Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vDAo9bzLca+QWfhOb9OLMg
  • edit in src/lib/Hydra/Schema/Jobsets.pm at line 186
    [10.21696]
    [83.19217]
    =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,
    );
  • replacement in src/lib/Hydra/Schema/Jobsets.pm at line 341
    [10.22628][3.0:142]()
    # 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
    [10.22628]
    [10.217]
    # Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Coci9FdBAvUO9T3st2NEqA
  • edit in src/lib/Hydra/Schema/Projects.pm at line 108
    [10.24478]
    [84.4154]
    =head2 buildmetrics
    Type: has_many
    Related object: L<Hydra::Schema::BuildMetrics>
  • edit in src/lib/Hydra/Schema/Projects.pm at line 115
    [84.4155]
    [10.24479]
    =cut
    __PACKAGE__->has_many(
    "buildmetrics",
    "Hydra::Schema::BuildMetrics",
    { "foreign.project" => "self.name" },
    undef,
    );
  • replacement in src/lib/Hydra/Schema/Projects.pm at line 285
    [10.7308][3.173:315]()
    # Created by DBIx::Class::Schema::Loader v0.07033 @ 2014-04-23 23:13:08
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:fkd9ruEoVSBGIktmAj4u4g
    [10.7308]
    [10.171]
    # Created by DBIx::Class::Schema::Loader v0.07043 @ 2015-07-30 16:52:20
    # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:67kWIE0IGmEJTvOIATAKaw
  • replacement in src/root/build.tt at line 109
    [10.1217][10.0:3]()
    [10.1217]
    [10.1530]
  • edit in src/root/build.tt at line 388
    [10.10758]
    [10.10758]
    [% 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 %]
  • replacement in src/root/common.tt at line 569
    [10.809][10.809:832]()
    var max = 0;
    [10.809]
    [10.832]
    var maxTime = 0;
    var minTime = Number.MAX_SAFE_INTEGER;
  • replacement in src/root/common.tt at line 575
    [4.636][10.990:1026](),[10.990][10.990:1026]()
    max = Math.max(t, max);
    [4.636]
    [10.1026]
    maxTime = Math.max(t, maxTime);
    minTime = Math.min(t, minTime);
  • replacement in src/root/common.tt at line 639
    [10.2763][10.2763:2854]()
    plot.setSelection({ xaxis: { from: max - 60 * 24 * 60 * 60 * 1000, to: max } });
    [10.2763]
    [10.2854]
    plot.setSelection({ xaxis: { from: Math.max(minTime, maxTime - 60 * 24 * 60 * 60 * 1000), to: maxTime } });
  • edit in src/root/job.tt at line 100
    [4.1081]
    [86.3469]
    [% 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 %]
  • file addition: metric.tt (----------)
    [80.1486]
    [% WRAPPER layout.tt title="Job metric ‘$metricName’" %]
    [% PROCESS common.tt %]
    [% INCLUDE includeFlot %]
    [% INCLUDE createChart id="chart" dataUrl=c.req.uri %]
    [% END %]
  • edit in src/root/static/css/hydra.css at line 30
    [87.81]
    [87.81]
    }
    table.table-small {
    width: auto !important;
  • edit in src/sql/hydra.sql at line 325
    [88.1028]
    [88.1028]
    create table BuildMetrics (
    build integer not null,
    name text not null,
  • edit in src/sql/hydra.sql at line 331
    [88.1029]
    [88.1029]
    unit text,
    value double precision not null,
  • edit in src/sql/hydra.sql at line 334
    [88.1030]
    [10.4327]
    -- 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
    );
  • edit in src/sql/hydra.sql at line 615
    [89.2243]
    [89.2243]
    create index IndexBuildMetricsOnJobTimestamp on BuildMetrics(project, jobset, job, timestamp desc);
  • file addition: upgrade-39.sql (----------)
    [80.3004]
    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);