diff --git a/lib/api_projects/tests/projects.rs b/lib/api_projects/tests/projects.rs index 61fec52a99..c29dd16374 100644 --- a/lib/api_projects/tests/projects.rs +++ b/lib/api_projects/tests/projects.rs @@ -7,7 +7,7 @@ //! Integration tests for project CRUD endpoints. use bencher_api_tests::TestServer; -use bencher_json::{JsonNewProject, JsonProject, JsonProjects, ProjectUuid}; +use bencher_json::{JsonNewProject, JsonProject, JsonProjects, ParameterFilter, ProjectUuid}; use http::StatusCode; // GET /v0/projects - list all public projects @@ -1293,3 +1293,172 @@ async fn projects_update_private_for_a_paid_org_past_its_daily_metrics_limit() { let resp_body = resp.text().await.expect("Failed to read response body"); assert_eq!(status, StatusCode::OK, "{resp_body}"); } + +// A plot's parameters filter is stored in its canonical form. +#[tokio::test] +async fn plot_parameters_round_trip_canonical() { + use bencher_api_tests::helpers::get_project_id; + + let server = TestServer::new().await; + let user = server + .signup("Plot User Params", "plotparams@example.com") + .await; + let org = server.create_org(&user, "Plot Org Params").await; + let project = server + .create_project(&user, &org, "Plot Project Params") + .await; + let project_slug: &str = project.slug.as_ref(); + let project_id = get_project_id(&server, project_slug); + + let dims = seed_plot_dimensions(&server, project_id); + // Scrambled order and two spellings of one number go in. + let created = post_plot( + &server, + &user, + project_slug, + &serde_json::json!({ + "lower_value": true, + "upper_value": true, + "lower_boundary": false, + "upper_boundary": false, + "x_axis": "date_time", + "window": 2_592_000, + "branches": [dims.branch1.to_string()], + "testbeds": [dims.testbed.to_string()], + "benchmarks": [dims.benchmark.to_string()], + "parameters": [ + { "size": 2 }, + { "size": 1.0 }, + { "size": 1 }, + ], + "measures": [dims.measure.to_string()], + }), + ) + .await; + // The canonical form comes back: sorted by canonical bytes, deduplicated. + let parameters = created.parameters.as_ref().expect("plot has a filter"); + assert_eq!(parameters.canonical(), r#"[{"size":1},{"size":2}]"#); + + // A read back through GET reports the same canonical filter. + let fetched: bencher_json::JsonPlot = server + .client + .get(server.api_url(&format!( + "/v0/projects/{project_slug}/plots/{}", + created.uuid + ))) + .header( + bencher_json::AUTHORIZATION, + bencher_json::bearer_header(&user.token), + ) + .send() + .await + .expect("Request failed") + .json() + .await + .expect("Failed to parse plot"); + assert_eq!( + fetched.parameters.as_ref().map(ParameterFilter::canonical), + Some(r#"[{"size":1},{"size":2}]"#.to_owned()) + ); + + // A patch replaces the filter wholesale. + let patch = serde_json::json!({ "parameters": [{ "size": 4, "threads": 8 }] }); + let updated = patch_plot(&server, &user, project_slug, created.uuid, &patch).await; + assert_eq!( + updated.parameters.as_ref().map(ParameterFilter::canonical), + Some(r#"[{"size":4,"threads":8}]"#.to_owned()) + ); + + // A patch that says nothing about the filter leaves it alone. + let untouched = patch_plot( + &server, + &user, + project_slug, + created.uuid, + &serde_json::json!({ "upper_boundary": true }), + ) + .await; + assert!(untouched.upper_boundary); + assert_eq!( + untouched + .parameters + .as_ref() + .map(ParameterFilter::canonical), + Some(r#"[{"size":4,"threads":8}]"#.to_owned()) + ); +} + +// A filter that matches every variant is no filter at all: `null`, the empty +// list, and the list holding the empty set are one stored state, and so is a plot +// created without a filter at all. +#[tokio::test] +async fn plot_parameters_match_all_is_absent() { + use bencher_api_tests::helpers::get_project_id; + + let server = TestServer::new().await; + let user = server + .signup("Plot User Match All", "plotmatchall@example.com") + .await; + let org = server.create_org(&user, "Plot Org Match All").await; + let project = server + .create_project(&user, &org, "Plot Project Match All") + .await; + let project_slug: &str = project.slug.as_ref(); + let project_id = get_project_id(&server, project_slug); + + let dims = seed_plot_dimensions(&server, project_id); + let new_plot = |parameters: Option| { + let mut plot = serde_json::json!({ + "lower_value": true, + "upper_value": true, + "lower_boundary": false, + "upper_boundary": false, + "x_axis": "date_time", + "window": 2_592_000, + "branches": [dims.branch1.to_string()], + "testbeds": [dims.testbed.to_string()], + "benchmarks": [dims.benchmark.to_string()], + "measures": [dims.measure.to_string()], + }); + if let Some(parameters) = parameters { + plot["parameters"] = parameters; + } + plot + }; + + // Absent, the empty list, and the list holding the empty set all create a plot + // with no filter, and the response leaves the field out. + for parameters in [ + None, + Some(serde_json::json!([])), + Some(serde_json::json!([{}])), + Some(serde_json::Value::Null), + ] { + let created = post_plot(&server, &user, project_slug, &new_plot(parameters)).await; + assert!(created.parameters.is_none()); + let body = serde_json::to_value(&created).expect("Failed to serialize plot"); + assert!(body.get("parameters").is_none()); + } + + // A plot that names a filter goes back to every variant on a `null` patch, + // and the same way on an empty list patch. + for clear in [serde_json::Value::Null, serde_json::json!([])] { + let created = post_plot( + &server, + &user, + project_slug, + &new_plot(Some(serde_json::json!([{ "size": 1 }]))), + ) + .await; + assert!(created.parameters.is_some()); + let cleared = patch_plot( + &server, + &user, + project_slug, + created.uuid, + &serde_json::json!({ "parameters": clear }), + ) + .await; + assert!(cleared.parameters.is_none()); + } +} diff --git a/lib/bencher_json/src/project/plot.rs b/lib/bencher_json/src/project/plot.rs index f5f5a5ac61..f8360a7eab 100644 --- a/lib/bencher_json/src/project/plot.rs +++ b/lib/bencher_json/src/project/plot.rs @@ -8,7 +8,7 @@ use serde::{ de::{self, Visitor}, }; -use crate::{BenchmarkUuid, BranchUuid, MeasureUuid, ProjectUuid, TestbedUuid}; +use crate::{BenchmarkUuid, BranchUuid, MeasureUuid, ParameterFilter, ProjectUuid, TestbedUuid}; crate::typed_uuid::typed_uuid!(PlotUuid); @@ -52,6 +52,11 @@ pub struct JsonNewPlot { /// The benchmarks to include in the plot. /// At least one benchmark must be specified. pub benchmarks: Vec, + /// The variants to include in the plot, as a parameters filter. + /// A variant matches when any entry in the filter is a subset of its parameters. + /// If not set, or set to an empty list, the plot includes every variant. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parameters: Option, /// The measures to include in the plot. /// At least one measure must be specified. pub measures: Vec, @@ -84,6 +89,10 @@ pub struct JsonPlot { pub branches: Vec, pub testbeds: Vec, pub benchmarks: Vec, + /// The variants this plot draws, in canonical order. + /// Absent when the plot draws every variant. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parameters: Option, pub measures: Vec, pub created: DateTime, pub modified: DateTime, @@ -134,6 +143,11 @@ pub struct JsonPlotPatch { /// Replaces the current benchmarks for the plot. /// At least one benchmark must be specified. pub benchmarks: Option>, + /// The variants to include in the plot, as a parameters filter. + /// Replaces the current filter for the plot. + /// Set to `null` or to an empty list to include every variant again. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option, /// The measures to include in the plot. /// Replaces the current measures for the plot. /// At least one measure must be specified. @@ -155,6 +169,8 @@ pub struct JsonPlotPatchNull { pub branches: Option>, pub testbeds: Option>, pub benchmarks: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option, pub measures: Option>, } @@ -176,6 +192,7 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { const BRANCHES_FIELD: &str = "branches"; const TESTBEDS_FIELD: &str = "testbeds"; const BENCHMARKS_FIELD: &str = "benchmarks"; + const PARAMETERS_FIELD: &str = "parameters"; const MEASURES_FIELD: &str = "measures"; const FIELDS: &[&str] = &[ INDEX_FIELD, @@ -190,6 +207,7 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { BRANCHES_FIELD, TESTBEDS_FIELD, BENCHMARKS_FIELD, + PARAMETERS_FIELD, MEASURES_FIELD, ]; @@ -208,6 +226,7 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { Branches, Testbeds, Benchmarks, + Parameters, Measures, } @@ -237,6 +256,7 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { let mut branches = None; let mut testbeds = None; let mut benchmarks = None; + let mut parameters: Option> = None; let mut measures = None; while let Some(key) = map.next_key()? { @@ -313,6 +333,12 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { } benchmarks = Some(map.next_value()?); }, + Field::Parameters => { + if parameters.is_some() { + return Err(de::Error::duplicate_field(PARAMETERS_FIELD)); + } + parameters = Some(map.next_value()?); + }, Field::Measures => { if measures.is_some() { return Err(de::Error::duplicate_field(MEASURES_FIELD)); @@ -322,6 +348,9 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { } } + // An explicit null clears the filter, as an empty list does. + let parameters = parameters.map(Option::unwrap_or_default); + Ok(match title { Some(Some(title)) => Self::Value::Patch(JsonPlotPatch { index, @@ -336,6 +365,7 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { branches, testbeds, benchmarks, + parameters, measures, }), Some(None) => Self::Value::Null(JsonPlotPatchNull { @@ -351,6 +381,7 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { branches, testbeds, benchmarks, + parameters, measures, }), None => Self::Value::Patch(JsonPlotPatch { @@ -366,6 +397,7 @@ impl<'de> Deserialize<'de> for JsonUpdatePlot { branches, testbeds, benchmarks, + parameters, measures, }), }) @@ -575,6 +607,7 @@ mod tests { assert!(patch.branches.is_none()); assert!(patch.testbeds.is_none()); assert!(patch.benchmarks.is_none()); + assert!(patch.parameters.is_none()); assert!(patch.measures.is_none()); } @@ -660,4 +693,33 @@ mod tests { serde_json::from_str::(r#"{"lower_value": true, "lower_value": false}"#) .unwrap_err(); } + + #[test] + fn deserialize_null_and_empty_parameters_both_clear() { + for body in [r#"{"parameters": null}"#, r#"{"parameters": []}"#] { + let update: JsonUpdatePlot = serde_json::from_str(body).unwrap(); + let JsonUpdatePlot::Patch(patch) = update else { + panic!("expected Patch variant"); + }; + let parameters = patch.parameters.expect("parameters was written"); + assert!(parameters.is_match_all(), "{body}"); + } + } + + #[test] + fn deserialize_duplicate_parameters_field_errors() { + serde_json::from_str::(r#"{"parameters": [], "parameters": []}"#) + .unwrap_err(); + } + + #[test] + fn deserialize_null_title_carries_parameters() { + let update: JsonUpdatePlot = + serde_json::from_str(r#"{"title": null, "parameters": [{"size": 1}]}"#).unwrap(); + let JsonUpdatePlot::Null(patch) = update else { + panic!("expected Null variant"); + }; + let parameters = patch.parameters.expect("parameters was written"); + assert_eq!(parameters.canonical(), r#"[{"size":1}]"#); + } } diff --git a/lib/bencher_schema/migrations/2026-10-01-130000_plot_parameters/down.sql b/lib/bencher_schema/migrations/2026-10-01-130000_plot_parameters/down.sql new file mode 100644 index 0000000000..1270755abc --- /dev/null +++ b/lib/bencher_schema/migrations/2026-10-01-130000_plot_parameters/down.sql @@ -0,0 +1,61 @@ +-- A plot that names a filter loses it and draws every variant again. +PRAGMA foreign_keys = off; + +CREATE TABLE down_plot ( + id INTEGER PRIMARY KEY NOT NULL, + uuid TEXT NOT NULL UNIQUE, + project_id INTEGER NOT NULL, + rank BIGINT NOT NULL, + title TEXT, + lower_value BOOLEAN NOT NULL, + upper_value BOOLEAN NOT NULL, + lower_boundary BOOLEAN NOT NULL, + upper_boundary BOOLEAN NOT NULL, + x_axis INTEGER NOT NULL, + y_axis INTEGER NOT NULL, + window BIGINT NOT NULL, + created BIGINT NOT NULL, + modified BIGINT NOT NULL, + FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE +); + +INSERT INTO down_plot( + id, + uuid, + project_id, + rank, + title, + lower_value, + upper_value, + lower_boundary, + upper_boundary, + x_axis, + y_axis, + window, + created, + modified + ) +SELECT id, + uuid, + project_id, + rank, + title, + lower_value, + upper_value, + lower_boundary, + upper_boundary, + x_axis, + y_axis, + window, + created, + modified +FROM plot; + +DROP TABLE plot; + +ALTER TABLE down_plot + RENAME TO plot; + +CREATE INDEX index_plot_project_created ON plot(project_id, created); + +PRAGMA foreign_keys = on; diff --git a/lib/bencher_schema/migrations/2026-10-01-130000_plot_parameters/up.sql b/lib/bencher_schema/migrations/2026-10-01-130000_plot_parameters/up.sql new file mode 100644 index 0000000000..20419df331 --- /dev/null +++ b/lib/bencher_schema/migrations/2026-10-01-130000_plot_parameters/up.sql @@ -0,0 +1,64 @@ +-- NULL is match all, so every existing plot keeps drawing every variant. +PRAGMA foreign_keys = off; + +CREATE TABLE up_plot ( + id INTEGER PRIMARY KEY NOT NULL, + uuid TEXT NOT NULL UNIQUE, + project_id INTEGER NOT NULL, + rank BIGINT NOT NULL, + title TEXT, + lower_value BOOLEAN NOT NULL, + upper_value BOOLEAN NOT NULL, + lower_boundary BOOLEAN NOT NULL, + upper_boundary BOOLEAN NOT NULL, + x_axis INTEGER NOT NULL, + y_axis INTEGER NOT NULL, + window BIGINT NOT NULL, + parameters BLOB, + created BIGINT NOT NULL, + modified BIGINT NOT NULL, + FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE +); + +INSERT INTO up_plot( + id, + uuid, + project_id, + rank, + title, + lower_value, + upper_value, + lower_boundary, + upper_boundary, + x_axis, + y_axis, + window, + parameters, + created, + modified + ) +SELECT id, + uuid, + project_id, + rank, + title, + lower_value, + upper_value, + lower_boundary, + upper_boundary, + x_axis, + y_axis, + window, + NULL, + created, + modified +FROM plot; + +DROP TABLE plot; + +ALTER TABLE up_plot + RENAME TO plot; + +CREATE INDEX index_plot_project_created ON plot(project_id, created); + +PRAGMA foreign_keys = on; diff --git a/lib/bencher_schema/src/model/project/plot/mod.rs b/lib/bencher_schema/src/model/project/plot/mod.rs index 005eba4de3..ca6441644f 100644 --- a/lib/bencher_schema/src/model/project/plot/mod.rs +++ b/lib/bencher_schema/src/model/project/plot/mod.rs @@ -1,6 +1,6 @@ use bencher_json::{ - BenchmarkUuid, BranchUuid, DateTime, Index, JsonNewPlot, JsonPlot, MeasureUuid, PlotUuid, - ResourceName, TestbedUuid, Window, + BenchmarkUuid, BranchUuid, DateTime, Index, JsonNewPlot, JsonPlot, MeasureUuid, + ParameterFilter, PlotUuid, ResourceName, TestbedUuid, Window, project::plot::{JsonPlotPatch, JsonPlotPatchNull, JsonUpdatePlot, XAxis, YAxis}, }; use bencher_rank::{Rank, RankGenerator, Ranked}; @@ -90,6 +90,7 @@ pub struct QueryPlot { pub x_axis: XAxis, pub y_axis: YAxis, pub window: Window, + pub parameters: Option, pub created: DateTime, pub modified: DateTime, } @@ -158,6 +159,7 @@ impl QueryPlot { x_axis: None, y_axis: None, window: None, + parameters: None, modified: now, }; diesel::update(plot_table::table.filter(plot_table::id.eq(plot.id))) @@ -216,6 +218,7 @@ impl QueryPlot { x_axis: None, y_axis: None, window: None, + parameters: None, modified, }; diesel::update(plot_table::table.filter(plot_table::id.eq(plot.id))) @@ -256,6 +259,7 @@ impl QueryPlot { x_axis, y_axis, window, + parameters, created, modified, .. @@ -274,6 +278,7 @@ impl QueryPlot { branches, testbeds, benchmarks, + parameters, measures, created, modified, @@ -299,6 +304,7 @@ impl QueryPlot { branches, testbeds, benchmarks, + parameters, measures, } = update.into(); @@ -366,6 +372,7 @@ impl QueryPlot { x_axis, y_axis, window, + parameters: parameters.map(canonical_parameters), modified, }; Self::apply_update( @@ -452,6 +459,7 @@ pub struct InsertPlot { pub x_axis: XAxis, pub y_axis: YAxis, pub window: Window, + pub parameters: Option, pub created: DateTime, pub modified: DateTime, } @@ -478,6 +486,7 @@ impl InsertPlot { branches, testbeds, benchmarks, + parameters, measures, } = plot; @@ -512,6 +521,7 @@ impl InsertPlot { x_axis, y_axis, window, + parameters: parameters.and_then(canonical_parameters), created: timestamp, modified: timestamp, }; @@ -551,9 +561,14 @@ pub struct UpdatePlot { pub x_axis: Option, pub y_axis: Option, pub window: Option, + pub parameters: Option>, pub modified: DateTime, } +fn canonical_parameters(parameters: ParameterFilter) -> Option { + (!parameters.is_match_all()).then_some(parameters) +} + /// The fields of a [`JsonUpdatePlot`], unified across its `Patch` and `Null` /// variants. A `Some(None)` title clears the current title. #[expect( @@ -573,6 +588,7 @@ struct UpdatePlotFields { branches: Option>, testbeds: Option>, benchmarks: Option>, + parameters: Option, measures: Option>, } @@ -593,6 +609,7 @@ impl From for UpdatePlotFields { branches, testbeds, benchmarks, + parameters, measures, } = patch; Self { @@ -608,6 +625,7 @@ impl From for UpdatePlotFields { branches, testbeds, benchmarks, + parameters, measures, } }, @@ -625,6 +643,7 @@ impl From for UpdatePlotFields { branches, testbeds, benchmarks, + parameters, measures, } = patch_null; Self { @@ -640,6 +659,7 @@ impl From for UpdatePlotFields { branches, testbeds, benchmarks, + parameters, measures, } }, @@ -953,6 +973,7 @@ mod tests { x_axis: XAxis::DateTime, y_axis: YAxis::Auto, window: bencher_json::Window::try_from(2_592_000u32).unwrap(), + parameters: None, created: timestamp, modified: timestamp, }; @@ -1129,6 +1150,7 @@ mod tests { x_axis: None, y_axis: None, window: None, + parameters: None, modified: DateTime::TEST, } } @@ -1198,6 +1220,7 @@ mod tests { x_axis: Some(XAxis::Version), y_axis: Some(YAxis::Log), window: None, + parameters: None, modified: DateTime::TEST, }; QueryPlot::apply_update(&mut conn, plot_id, &update_plot, None, None, None, None) @@ -1243,6 +1266,7 @@ mod tests { x_axis: XAxis::DateTime, y_axis: YAxis::Log, window: bencher_json::Window::try_from(2_592_000u32).unwrap(), + parameters: None, created: timestamp, modified: timestamp, }; diff --git a/lib/bencher_schema/src/schema.rs b/lib/bencher_schema/src/schema.rs index d6c2ec5564..67fb6c07a6 100644 --- a/lib/bencher_schema/src/schema.rs +++ b/lib/bencher_schema/src/schema.rs @@ -213,6 +213,7 @@ diesel::table! { x_axis -> Integer, y_axis -> Integer, window -> BigInt, + parameters -> Nullable, created -> BigInt, modified -> BigInt, } diff --git a/services/api/openapi.json b/services/api/openapi.json index 14c2174cf4..96fb532596 100644 --- a/services/api/openapi.json +++ b/services/api/openapi.json @@ -14360,6 +14360,15 @@ "$ref": "#/components/schemas/MeasureUuid" } }, + "parameters": { + "nullable": true, + "description": "The variants to include in the plot, as a parameters filter. A variant matches when any entry in the filter is a subset of its parameters. If not set, or set to an empty list, the plot includes every variant.", + "allOf": [ + { + "$ref": "#/components/schemas/ParameterFilter" + } + ] + }, "testbeds": { "description": "The testbeds to include in the plot. At least one testbed must be specified.", "type": "array", @@ -15844,6 +15853,15 @@ "modified": { "$ref": "#/components/schemas/DateTime" }, + "parameters": { + "nullable": true, + "description": "The variants this plot draws, in canonical order. Absent when the plot draws every variant.", + "allOf": [ + { + "$ref": "#/components/schemas/ParameterFilter" + } + ] + }, "project": { "$ref": "#/components/schemas/ProjectUuid" }, @@ -15944,6 +15962,15 @@ "$ref": "#/components/schemas/MeasureUuid" } }, + "parameters": { + "nullable": true, + "description": "The variants to include in the plot, as a parameters filter. Replaces the current filter for the plot. Set to `null` or to an empty list to include every variant again.", + "allOf": [ + { + "$ref": "#/components/schemas/ParameterFilter" + } + ] + }, "testbeds": { "nullable": true, "description": "The testbeds to include in the plot. Replaces the current testbeds for the plot. At least one testbed must be specified.", @@ -16040,6 +16067,14 @@ "$ref": "#/components/schemas/MeasureUuid" } }, + "parameters": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ParameterFilter" + } + ] + }, "testbeds": { "nullable": true, "type": "array", diff --git a/services/cli/src/bencher/sub/project/plot/create.rs b/services/cli/src/bencher/sub/project/plot/create.rs index f6514830e8..aa90db6256 100644 --- a/services/cli/src/bencher/sub/project/plot/create.rs +++ b/services/cli/src/bencher/sub/project/plot/create.rs @@ -120,6 +120,7 @@ impl From for JsonNewPlot { branches: branches.into_iter().map(Into::into).collect(), testbeds: testbeds.into_iter().map(Into::into).collect(), benchmarks: benchmarks.into_iter().map(Into::into).collect(), + parameters: None, measures: measures.into_iter().map(Into::into).collect(), } } diff --git a/services/cli/src/bencher/sub/project/plot/update.rs b/services/cli/src/bencher/sub/project/plot/update.rs index 33762d6aca..0611a5025c 100644 --- a/services/cli/src/bencher/sub/project/plot/update.rs +++ b/services/cli/src/bencher/sub/project/plot/update.rs @@ -134,6 +134,7 @@ impl From for JsonUpdatePlot { branches, testbeds, benchmarks, + parameters: None, measures, }), subtype_1: None, @@ -153,6 +154,7 @@ impl From for JsonUpdatePlot { branches, testbeds, benchmarks, + parameters: None, measures, }), }, @@ -170,6 +172,7 @@ impl From for JsonUpdatePlot { branches, testbeds, benchmarks, + parameters: None, measures, }), subtype_1: None, diff --git a/services/console/src/types/bencher.ts b/services/console/src/types/bencher.ts index 97b736a823..20d87fa01d 100644 --- a/services/console/src/types/bencher.ts +++ b/services/console/src/types/bencher.ts @@ -828,6 +828,12 @@ export interface JsonNewPlot { * At least one benchmark must be specified. */ benchmarks: Uuid[]; + /** + * The variants to include in the plot, as a parameters filter. + * A variant matches when any entry in the filter is a subset of its parameters. + * If not set, or set to an empty list, the plot includes every variant. + */ + parameters?: Record[]; /** * The measures to include in the plot. * At least one measure must be specified. @@ -1235,6 +1241,11 @@ export interface JsonPlot { branches: Uuid[]; testbeds: Uuid[]; benchmarks: Uuid[]; + /** + * The variants this plot draws, in canonical order. + * Absent when the plot draws every variant. + */ + parameters?: Record[]; measures: Uuid[]; created: string; modified: string;