From ea238558973158c697022900130b1c3672724caf Mon Sep 17 00:00:00 2001 From: Everett Pompeii Date: Tue, 7 Jul 2026 20:59:36 +0000 Subject: [PATCH] Speed up large report deletions Deleting an incident-scale report (hundreds of thousands of cascade rows) held the single SQLite writer connection for tens of seconds, blocking all other writes and generating a large WAL burst for Litestream to replicate. Validated against a copy of the production database: - Delete report results in bounded chunks (1024 report_benchmark rows per write statement) so the writer lock is released between chunks. Max writer stall drops from tens of seconds to about a second; other writers interleave. - Mint time-ordered UUIDv7 instead of UUIDv4 for typed uuids. Random v4 keys scatter one dirty page per row across the uuid indexes on bulk insert and delete (the large majority of the WAL churn); v7 clusters same-batch rows. - Add a configurable SQLite page cache size for the writer connection (default 64 MiB, was the 2 MiB SQLite default) to avoid re-reading evicted pages during large deletes and ingests. - Drop two redundant report_benchmark indexes: index_report_benchmark_benchmark is a strict prefix of index_report_benchmark_benchmark_report, and index_report_benchmark is covered by the UNIQUE(report_id, iteration, benchmark_id) autoindex. Verified query plans are unaffected. - Move the otel ReportDelete counter before the early return so all report deletes are counted, not just those that drop a dangling version. --- CLAUDE.md | 4 + lib/api_projects/src/lib.rs | 2 + lib/api_projects/src/reports.rs | 52 +++- lib/api_projects/tests/reports.rs | 293 +++++++++++++++++- lib/api_run/Cargo.toml | 2 +- lib/bencher_config/src/config_tx.rs | 11 +- lib/bencher_config/src/lib.rs | 10 + lib/bencher_json/Cargo.toml | 2 +- .../src/system/config/database.rs | 5 +- lib/bencher_json/src/typed_uuid.rs | 16 +- .../down.sql | 2 + .../up.sql | 4 + services/api/openapi.json | 7 + services/cli/Cargo.toml | 2 +- .../docs-reference/changelog/en/changelog.mdx | 5 + .../server-config/de/database.mdx | 1 + .../server-config/en/database.mdx | 1 + .../server-config/es/database.mdx | 1 + .../docs-reference/server-config/example.mdx | 3 +- .../server-config/fr/database.mdx | 1 + .../server-config/ja/database.mdx | 1 + .../server-config/ko/database.mdx | 1 + .../server-config/pt/database.mdx | 1 + .../server-config/ru/database.mdx | 1 + .../server-config/zh/database.mdx | 1 + .../content/docs-reference/de/changelog.mdx | 2 +- .../docs-reference/de/server-config.mdx | 2 +- .../content/docs-reference/en/changelog.mdx | 2 +- .../docs-reference/en/server-config.mdx | 2 +- .../content/docs-reference/es/changelog.mdx | 2 +- .../docs-reference/es/server-config.mdx | 2 +- .../content/docs-reference/fr/changelog.mdx | 2 +- .../docs-reference/fr/server-config.mdx | 2 +- .../content/docs-reference/ja/changelog.mdx | 2 +- .../docs-reference/ja/server-config.mdx | 2 +- .../content/docs-reference/ko/changelog.mdx | 2 +- .../docs-reference/ko/server-config.mdx | 2 +- .../content/docs-reference/pt/changelog.mdx | 2 +- .../docs-reference/pt/server-config.mdx | 2 +- .../content/docs-reference/ru/changelog.mdx | 2 +- .../docs-reference/ru/server-config.mdx | 2 +- .../content/docs-reference/zh/changelog.mdx | 2 +- .../docs-reference/zh/server-config.mdx | 2 +- 43 files changed, 433 insertions(+), 32 deletions(-) create mode 100644 lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/down.sql create mode 100644 lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/up.sql diff --git a/CLAUDE.md b/CLAUDE.md index 0696add956..9ddc3d580d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,10 @@ Version control uses Jujutsu (`jj`) with Git. - Do **NOT** add `Co-Authored-By` lines to commit messages - Do **NOT** add `🤖 Generated with [Claude Code](https://claude.com/claude-code)` to PR descriptions +## Public Disclosure + +This is a public repository. **NEVER** disclose sensitive information about the production setup, data, or operations in any public-facing artifact: PR descriptions, PR/issue comments, commit messages, code comments, or docs. This includes production database sizes, row counts, table statistics, customer or project identifiers, incident details and timelines, and measured production performance characteristics. Describe changes in relative terms (e.g., "a large report" instead of actual production numbers) and keep concrete production figures in private channels. + ## Writing Style - **NEVER** use emdashes in any writing output: code comments, commit messages, PR descriptions, docs, issue/PR replies, and chat responses. Use a colon, semicolon, comma, parentheses, or two sentences instead. diff --git a/lib/api_projects/src/lib.rs b/lib/api_projects/src/lib.rs index 6bc09f197d..fd20d04a05 100644 --- a/lib/api_projects/src/lib.rs +++ b/lib/api_projects/src/lib.rs @@ -23,6 +23,8 @@ mod thresholds; mod macros; +pub use reports::DELETE_CHUNK_SIZE; + pub struct Api; impl bencher_endpoint::Registrar for Api { diff --git a/lib/api_projects/src/reports.rs b/lib/api_projects/src/reports.rs index 0f4f1deb42..5063ecf8d0 100644 --- a/lib/api_projects/src/reports.rs +++ b/lib/api_projects/src/reports.rs @@ -29,7 +29,7 @@ use bencher_schema::{ head::HeadId, version::{QueryVersion, VersionId}, }, - report::{NewRunReport, QueryReport, ReportId}, + report::{NewRunReport, QueryReport, ReportId, report_benchmark::ReportBenchmarkId}, }, user::{ actor::{ApiActor, PubProjectBearerToken}, @@ -446,6 +446,12 @@ pub async fn proj_report_delete( Ok(Delete::auth_response_deleted()) } +/// Number of `report_benchmark` rows deleted per write statement when +/// deleting a report's results. Bounds how long each delete holds the single +/// writer connection (validated at ~1s per chunk against a production-scale +/// report). +pub const DELETE_CHUNK_SIZE: i64 = 1024; + async fn delete_inner( context: &ApiContext, path_params: ProjReportParams, @@ -470,10 +476,15 @@ async fn delete_inner( Report, (&query_project, path_params.report) ))?; + delete_report_results(context, &query_project, report_id).await?; + diesel::delete(schema::report::table.filter(schema::report::id.eq(report_id))) .execute(write_conn!(context)) .map_err(resource_conflict_err!(Report, report_id))?; + #[cfg(feature = "otel")] + bencher_otel::ApiMeter::increment(bencher_otel::ApiCounter::ReportDelete); + // If there are no more reports for this version, delete the version // This is necessary because multiple reports can use the same version via a git hash // This will cascade and delete all head versions for this version @@ -549,8 +560,41 @@ async fn delete_inner( (&query_project, report_id, &query_version) ))?; - #[cfg(feature = "otel")] - bencher_otel::ApiMeter::increment(bencher_otel::ApiCounter::ReportDelete); - Ok(()) } + +/// Delete a report's results in bounded chunks, each in its own write +/// statement, so a large report does not hold the single writer connection +/// for its entire cascade (`report_benchmark` -> metric -> boundary -> alert). +/// Other writers interleave between chunks. Any rows that land between the +/// final chunk and the report row delete are cleaned up by the report's own +/// cascade. +/// +/// The deletion is deliberately not atomic: if the server fails mid-loop, the +/// report remains visible with a partial set of results (and a stale +/// `metric_count_by_report` rollup) until the delete is retried. This is +/// acceptable because reports are immutable after creation, the delete is +/// idempotent, and the report was already condemned by the caller. +async fn delete_report_results( + context: &ApiContext, + query_project: &QueryProject, + report_id: ReportId, +) -> Result<(), HttpError> { + loop { + let report_benchmark_ids = schema::report_benchmark::table + .filter(schema::report_benchmark::report_id.eq(report_id)) + .select(schema::report_benchmark::id) + .limit(DELETE_CHUNK_SIZE) + .load::(auth_conn!(context)) + .map_err(resource_not_found_err!(Report, (query_project, report_id)))?; + if report_benchmark_ids.is_empty() { + return Ok(()); + } + diesel::delete( + schema::report_benchmark::table + .filter(schema::report_benchmark::id.eq_any(report_benchmark_ids)), + ) + .execute(write_conn!(context)) + .map_err(resource_conflict_err!(Report, report_id))?; + } +} diff --git a/lib/api_projects/tests/reports.rs b/lib/api_projects/tests/reports.rs index 31a7f446a3..f8d3379cd2 100644 --- a/lib/api_projects/tests/reports.rs +++ b/lib/api_projects/tests/reports.rs @@ -6,10 +6,299 @@ )] //! Integration tests for project report endpoints. -use bencher_api_tests::TestServer; -use bencher_json::JsonReports; +use bencher_api_tests::{ + TestServer, + helpers::{base_timestamp, create_test_report, get_project_id}, +}; +use bencher_json::{ + BenchmarkUuid, BoundaryUuid, JsonReports, MeasureUuid, MetricUuid, ModelUuid, + ReportBenchmarkUuid, ThresholdUuid, +}; +use bencher_schema::{ + context::DbConnection, + model::project::report::{ReportId, upsert_metric_count}, + schema, +}; +use diesel::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; use http::StatusCode; +/// Insert the benchmark, measure, threshold, and model rows needed for +/// report results. Returns (`benchmark_id`, `measure_id`, `threshold_id`, `model_id`). +#[expect(clippy::expect_used, reason = "test helper")] +fn seed_result_infra( + conn: &mut DbConnection, + project_id: i32, + branch_id: i32, + testbed_id: i32, +) -> (i32, i32, i32, i32) { + let now = base_timestamp(); + + let benchmark_uuid = BenchmarkUuid::new(); + diesel::insert_into(schema::benchmark::table) + .values(( + schema::benchmark::uuid.eq(&benchmark_uuid), + schema::benchmark::project_id.eq(project_id), + schema::benchmark::name.eq("test-benchmark"), + schema::benchmark::slug.eq(&format!("test-benchmark-{benchmark_uuid}")), + schema::benchmark::created.eq(&now), + schema::benchmark::modified.eq(&now), + )) + .execute(&mut *conn) + .expect("Failed to insert benchmark"); + let benchmark_id: i32 = schema::benchmark::table + .filter(schema::benchmark::uuid.eq(&benchmark_uuid)) + .select(schema::benchmark::id) + .first(&mut *conn) + .expect("Failed to get benchmark ID"); + + let measure_uuid = MeasureUuid::new(); + diesel::insert_into(schema::measure::table) + .values(( + schema::measure::uuid.eq(&measure_uuid), + schema::measure::project_id.eq(project_id), + schema::measure::name.eq("test-measure"), + schema::measure::slug.eq(&format!("test-measure-{measure_uuid}")), + schema::measure::units.eq("ns"), + schema::measure::created.eq(&now), + schema::measure::modified.eq(&now), + )) + .execute(&mut *conn) + .expect("Failed to insert measure"); + let measure_id: i32 = schema::measure::table + .filter(schema::measure::uuid.eq(&measure_uuid)) + .select(schema::measure::id) + .first(&mut *conn) + .expect("Failed to get measure ID"); + + let threshold_uuid = ThresholdUuid::new(); + diesel::insert_into(schema::threshold::table) + .values(( + schema::threshold::uuid.eq(&threshold_uuid), + schema::threshold::project_id.eq(project_id), + schema::threshold::branch_id.eq(branch_id), + schema::threshold::testbed_id.eq(testbed_id), + schema::threshold::measure_id.eq(measure_id), + schema::threshold::created.eq(&now), + schema::threshold::modified.eq(&now), + )) + .execute(&mut *conn) + .expect("Failed to insert threshold"); + let threshold_id: i32 = schema::threshold::table + .filter(schema::threshold::uuid.eq(&threshold_uuid)) + .select(schema::threshold::id) + .first(&mut *conn) + .expect("Failed to get threshold ID"); + + let model_uuid = ModelUuid::new(); + diesel::insert_into(schema::model::table) + .values(( + schema::model::uuid.eq(&model_uuid), + schema::model::threshold_id.eq(threshold_id), + schema::model::test.eq(0), + schema::model::created.eq(&now), + )) + .execute(&mut *conn) + .expect("Failed to insert model"); + let model_id: i32 = schema::model::table + .filter(schema::model::uuid.eq(&model_uuid)) + .select(schema::model::id) + .first(conn) + .expect("Failed to get model ID"); + + (benchmark_id, measure_id, threshold_id, model_id) +} + +/// Seed `count` report results (`report_benchmark` -> metric -> boundary rows) +/// for a report created via `create_test_report`. Uses one benchmark with +/// `count` iterations. Returns the report UUID for the delete URL. +#[expect(clippy::expect_used, reason = "test helper")] +fn seed_report_results(server: &TestServer, project_id: i32, report_id: i32, count: i32) -> String { + // Batch size per INSERT statement, kept well under SQLite's bind limit. + const INSERT_BATCH: usize = 1024; + + let mut conn = server.db_conn(); + + let (report_uuid, testbed_id, head_id): (String, i32, i32) = schema::report::table + .filter(schema::report::id.eq(report_id)) + .select(( + schema::report::uuid, + schema::report::testbed_id, + schema::report::head_id, + )) + .first(&mut conn) + .expect("Failed to get report"); + let branch_id: i32 = schema::head::table + .filter(schema::head::id.eq(head_id)) + .select(schema::head::branch_id) + .first(&mut conn) + .expect("Failed to get branch ID"); + + let (benchmark_id, measure_id, threshold_id, model_id) = + seed_result_infra(&mut conn, project_id, branch_id, testbed_id); + + let report_benchmarks = (0..count) + .map(|iteration| { + ( + schema::report_benchmark::uuid.eq(ReportBenchmarkUuid::new()), + schema::report_benchmark::report_id.eq(report_id), + schema::report_benchmark::iteration.eq(iteration), + schema::report_benchmark::benchmark_id.eq(benchmark_id), + ) + }) + .collect::>(); + for batch in report_benchmarks.chunks(INSERT_BATCH) { + diesel::insert_into(schema::report_benchmark::table) + .values(batch.to_vec()) + .execute(&mut conn) + .expect("Failed to insert report benchmarks"); + } + let report_benchmark_ids: Vec = schema::report_benchmark::table + .filter(schema::report_benchmark::report_id.eq(report_id)) + .select(schema::report_benchmark::id) + .load(&mut conn) + .expect("Failed to get report benchmark IDs"); + + let metrics = report_benchmark_ids + .iter() + .map(|report_benchmark_id| { + ( + schema::metric::uuid.eq(MetricUuid::new()), + schema::metric::report_benchmark_id.eq(*report_benchmark_id), + schema::metric::measure_id.eq(measure_id), + schema::metric::value.eq(42.0), + ) + }) + .collect::>(); + for batch in metrics.chunks(INSERT_BATCH) { + diesel::insert_into(schema::metric::table) + .values(batch.to_vec()) + .execute(&mut conn) + .expect("Failed to insert metrics"); + } + let metric_ids: Vec = schema::metric::table + .filter(schema::metric::report_benchmark_id.eq_any(&report_benchmark_ids)) + .select(schema::metric::id) + .load(&mut conn) + .expect("Failed to get metric IDs"); + + let boundaries = metric_ids + .iter() + .map(|metric_id| { + ( + schema::boundary::uuid.eq(BoundaryUuid::new()), + schema::boundary::metric_id.eq(*metric_id), + schema::boundary::threshold_id.eq(threshold_id), + schema::boundary::model_id.eq(model_id), + schema::boundary::baseline.eq(42.0), + schema::boundary::upper_limit.eq(100.0), + ) + }) + .collect::>(); + for batch in boundaries.chunks(INSERT_BATCH) { + diesel::insert_into(schema::boundary::table) + .values(batch.to_vec()) + .execute(&mut conn) + .expect("Failed to insert boundaries"); + } + + let report_id = ReportId::try_from_raw(report_id).expect("valid report ID"); + upsert_metric_count(&mut conn, report_id, count).expect("Failed to upsert metric count"); + + report_uuid +} + +/// Count the report, `report_benchmark`, metric, and boundary rows for a report. +#[expect(clippy::expect_used, reason = "test helper")] +fn report_row_counts(server: &TestServer, report_id: i32) -> (i64, i64, i64, i64) { + let mut conn = server.db_conn(); + let reports: i64 = schema::report::table + .filter(schema::report::id.eq(report_id)) + .count() + .first(&mut conn) + .expect("Failed to count reports"); + let report_benchmark_ids: Vec = schema::report_benchmark::table + .filter(schema::report_benchmark::report_id.eq(report_id)) + .select(schema::report_benchmark::id) + .load(&mut conn) + .expect("Failed to get report benchmark IDs"); + let metric_ids: Vec = schema::metric::table + .filter(schema::metric::report_benchmark_id.eq_any(&report_benchmark_ids)) + .select(schema::metric::id) + .load(&mut conn) + .expect("Failed to get metric IDs"); + let boundaries: i64 = schema::boundary::table + .filter(schema::boundary::metric_id.eq_any(&metric_ids)) + .count() + .first(&mut conn) + .expect("Failed to count boundaries"); + #[expect(clippy::cast_possible_wrap, reason = "test row counts")] + ( + reports, + report_benchmark_ids.len() as i64, + metric_ids.len() as i64, + boundaries, + ) +} + +/// Delete a report and assert that all of its result rows are gone. +#[expect(clippy::expect_used, reason = "test helper")] +async fn delete_report_and_assert_empty(count: i32) { + let server = TestServer::new().await; + let user = server + .signup("Test User", "reportdelresults@example.com") + .await; + let org = server.create_org(&user, "Report Del Results Org").await; + let project = server + .create_project(&user, &org, "Report Del Results Project") + .await; + + let project_slug: &str = project.slug.as_ref(); + let project_id = get_project_id(&server, project_slug); + let report_id = create_test_report(&server, project_id); + let report_uuid = seed_report_results(&server, project_id, report_id, count); + + let counts = report_row_counts(&server, report_id); + assert_eq!( + counts, + (1, i64::from(count), i64::from(count), i64::from(count)), + "seeded report rows" + ); + + let resp = server + .client + .delete(server.api_url(&format!( + "/v0/projects/{}/reports/{}", + project_slug, report_uuid + ))) + .header( + bencher_json::AUTHORIZATION, + bencher_json::bearer_header(&user.token), + ) + .send() + .await + .expect("Request failed"); + assert_eq!(resp.status(), StatusCode::NO_CONTENT, "delete report"); + + let counts = report_row_counts(&server, report_id); + assert_eq!(counts, (0, 0, 0, 0), "report rows after delete"); +} + +// DELETE /v0/projects/{project}/reports/{report} - report with results +// (fewer rows than one delete chunk) +#[tokio::test] +async fn reports_delete_with_results() { + delete_report_and_assert_empty(5).await; +} + +// DELETE /v0/projects/{project}/reports/{report} - report with exactly +// 2 * DELETE_CHUNK_SIZE results, exercising multiple delete chunks and the +// exact-multiple boundary condition +#[tokio::test] +async fn reports_delete_chunked() { + let count = i32::try_from(2 * api_projects::DELETE_CHUNK_SIZE).expect("count fits in i32"); + delete_report_and_assert_empty(count).await; +} + // GET /v0/projects/{project}/reports - list reports (empty) #[tokio::test] async fn reports_list_empty() { diff --git a/lib/api_run/Cargo.toml b/lib/api_run/Cargo.toml index 966a690fe8..cb6b2fe702 100644 --- a/lib/api_run/Cargo.toml +++ b/lib/api_run/Cargo.toml @@ -29,7 +29,7 @@ diesel.workspace = true http.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -uuid.workspace = true +uuid = { workspace = true, features = ["v4"] } [lints] workspace = true diff --git a/lib/bencher_config/src/config_tx.rs b/lib/bencher_config/src/config_tx.rs index 73ec6fe821..024301e75f 100644 --- a/lib/bencher_config/src/config_tx.rs +++ b/lib/bencher_config/src/config_tx.rs @@ -44,7 +44,7 @@ use slog::{Logger, debug, error, info}; #[cfg(feature = "plus")] use super::plus::Plus; -use super::{Config, DEFAULT_BUSY_TIMEOUT}; +use super::{Config, DEFAULT_BUSY_TIMEOUT, DEFAULT_CACHE_SIZE}; const DATABASE_URL: &str = "DATABASE_URL"; const SQLITE_TMPDIR: &str = "SQLITE_TMPDIR"; @@ -224,9 +224,10 @@ async fn into_context( // busy_timeout prevents immediate SQLITE_BUSY errors under lock contention. // synchronous=NORMAL is safe with WAL mode and reduces fsync overhead. let busy_timeout = json_database.busy_timeout.unwrap_or(DEFAULT_BUSY_TIMEOUT); + let cache_size = json_database.cache_size.unwrap_or(DEFAULT_CACHE_SIZE); info!( log, - "Setting database PRAGMAs (busy_timeout: {busy_timeout}ms)" + "Setting database PRAGMAs (busy_timeout: {busy_timeout}ms, cache_size: {cache_size} KiB)" ); database_connection .batch_execute("PRAGMA journal_mode = WAL") @@ -237,6 +238,12 @@ async fn into_context( database_connection .batch_execute("PRAGMA synchronous = NORMAL") .map_err(ConfigTxError::Pragma)?; + // Writer connection only: the read pools hold many connections, so a + // large per-connection cache would multiply memory, and reads are served + // by the OS page cache. Negative value = KiB in SQLite. + database_connection + .batch_execute(&format!("PRAGMA cache_size = -{cache_size}")) + .map_err(ConfigTxError::Pragma)?; // Surfaces `SQLITE_BUSY_SNAPSHOT` (517) etc. distinctly from plain `SQLITE_BUSY` (5) // in error messages, instead of both rendering as "database is locked". database_connection diff --git a/lib/bencher_config/src/lib.rs b/lib/bencher_config/src/lib.rs index 940056e62d..e69f378814 100644 --- a/lib/bencher_config/src/lib.rs +++ b/lib/bencher_config/src/lib.rs @@ -1,6 +1,7 @@ use std::sync::LazyLock; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, + num::NonZeroU32, ops::Deref, }; @@ -47,6 +48,14 @@ const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Info; const DEFAULT_BUSY_TIMEOUT: u32 = 5_000; +// 64 MiB, in KiB. Large report deletions and ingests dirty far more pages +// than the SQLite default (2 MiB); a bigger writer cache avoids re-reading +// evicted pages mid-operation. +const DEFAULT_CACHE_SIZE: NonZeroU32 = match NonZeroU32::new(0x1_0000) { + Some(cache_size) => cache_size, + None => panic!("default cache size is zero"), +}; + const DEFAULT_CONSOLE_URL_STR: &str = "http://localhost:3000"; #[expect(clippy::panic, reason = "compile-time constant URL must be valid")] static DEFAULT_CONSOLE_URL: LazyLock = LazyLock::new(|| { @@ -220,6 +229,7 @@ impl Default for Config { file: DEFAULT_DB_PATH.into(), data_store: None, busy_timeout: None, + cache_size: None, }, smtp: None, logging: JsonLogging { diff --git a/lib/bencher_json/Cargo.toml b/lib/bencher_json/Cargo.toml index 1c34124b3a..ee0632ea92 100644 --- a/lib/bencher_json/Cargo.toml +++ b/lib/bencher_json/Cargo.toml @@ -33,7 +33,7 @@ tabled = { workspace = true, optional = true } thiserror.workspace = true typeshare.workspace = true url = { workspace = true, features = ["serde"] } -uuid = { workspace = true, features = ["v4", "serde"] } +uuid = { workspace = true, features = ["v7", "serde"] } [dev-dependencies] pretty_assertions.workspace = true diff --git a/lib/bencher_json/src/system/config/database.rs b/lib/bencher_json/src/system/config/database.rs index 1fff4b4fb2..bd76ca664a 100644 --- a/lib/bencher_json/src/system/config/database.rs +++ b/lib/bencher_json/src/system/config/database.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{num::NonZeroU32, path::PathBuf}; use bencher_valid::{Sanitize, Secret}; #[cfg(feature = "schema")] @@ -14,6 +14,9 @@ pub struct JsonDatabase { /// The database busy timeout in milliseconds #[serde(skip_serializing_if = "Option::is_none")] pub busy_timeout: Option, + /// The database page cache size in KiB for the writer connection + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_size: Option, } impl Sanitize for JsonDatabase { diff --git a/lib/bencher_json/src/typed_uuid.rs b/lib/bencher_json/src/typed_uuid.rs index a621db9ca9..b63fb5cbc2 100644 --- a/lib/bencher_json/src/typed_uuid.rs +++ b/lib/bencher_json/src/typed_uuid.rs @@ -61,8 +61,11 @@ macro_rules! typed_uuid { } impl $uuid { + // Time-ordered UUIDv7 keeps same-batch rows adjacent in the + // database's uuid indexes, avoiding the B-tree page scatter that + // random v4 uuids cause on bulk insert and delete. pub fn new() -> Self { - Self(uuid::Uuid::new_v4()) + Self(uuid::Uuid::now_v7()) } } @@ -71,3 +74,14 @@ macro_rules! typed_uuid { } pub(crate) use typed_uuid; + +#[cfg(test)] +mod tests { + use crate::project::metric::MetricUuid; + + #[test] + fn typed_uuid_new_is_v7() { + let uuid: uuid::Uuid = MetricUuid::new().into(); + assert_eq!(uuid.get_version_num(), 7); + } +} diff --git a/lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/down.sql b/lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/down.sql new file mode 100644 index 0000000000..6d0f0c7508 --- /dev/null +++ b/lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX index_report_benchmark_benchmark ON report_benchmark(benchmark_id); +CREATE INDEX index_report_benchmark ON report_benchmark(report_id, benchmark_id); diff --git a/lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/up.sql b/lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/up.sql new file mode 100644 index 0000000000..62a627d1e2 --- /dev/null +++ b/lib/bencher_schema/migrations/2026-07-07-120000_report_benchmark_index_cleanup/up.sql @@ -0,0 +1,4 @@ +-- Redundant with index_report_benchmark_benchmark_report(benchmark_id, report_id) +DROP INDEX IF EXISTS index_report_benchmark_benchmark; +-- Redundant with the UNIQUE(report_id, iteration, benchmark_id) autoindex +DROP INDEX IF EXISTS index_report_benchmark; diff --git a/services/api/openapi.json b/services/api/openapi.json index eeca82ae37..cd08d09541 100644 --- a/services/api/openapi.json +++ b/services/api/openapi.json @@ -12657,6 +12657,13 @@ "format": "uint32", "minimum": 0 }, + "cache_size": { + "nullable": true, + "description": "The database page cache size in KiB for the writer connection", + "type": "integer", + "format": "uint32", + "minimum": 1 + }, "data_store": { "nullable": true, "allOf": [ diff --git a/services/cli/Cargo.toml b/services/cli/Cargo.toml index 1556572d9a..a6324cccfc 100644 --- a/services/cli/Cargo.toml +++ b/services/cli/Cargo.toml @@ -38,7 +38,7 @@ thiserror.workspace = true tokio = { workspace = true, features = ["macros", "process", "rt", "signal"] } tokio-rustls.workspace = true url.workspace = true -uuid.workspace = true +uuid = { workspace = true, features = ["v4"] } [lints] workspace = true diff --git a/services/console/src/chunks/docs-reference/changelog/en/changelog.mdx b/services/console/src/chunks/docs-reference/changelog/en/changelog.mdx index 8d68d2146f..9614fa332e 100644 --- a/services/console/src/chunks/docs-reference/changelog/en/changelog.mdx +++ b/services/console/src/chunks/docs-reference/changelog/en/changelog.mdx @@ -1,3 +1,8 @@ +## Pending `v0.6.9` +- Speed up large report deletions: delete report results in bounded chunks so a large report delete no longer blocks all other database writes for its full duration +- Mint time-ordered UUIDv7 instead of random UUIDv4 for new resources, clustering database index writes for bulk inserts and deletes +- Add a `database.cache_size` server config option for the writer connection page cache in KiB (default 64 MiB) + ## `v0.6.8` - **BREAKING CHANGE** Change the default self-hosted API server port from `61016` to the newly [IANA-registered](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=6610) `6610`. Deployments relying on the default now serve the API on `6610`. Update clients, reverse proxies, and `--host`/`BENCHER_HOST` references (Docker Compose, devcontainer, and docs are updated to match). To stay on `61016`, set `server.bind_address` to `0.0.0.0:61016` (or run `bencher up --api-port 61016`). - Mark the remaining user API token REST endpoints (list, view, update, and revoke) as deprecated in the OpenAPI spec and API docs; they continue to work for existing tokens diff --git a/services/console/src/chunks/docs-reference/server-config/de/database.mdx b/services/console/src/chunks/docs-reference/server-config/de/database.mdx index 5d5a3a964d..8c8c27849f 100644 --- a/services/console/src/chunks/docs-reference/server-config/de/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/de/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | Nur wenn data_store.service = "aws_s3" | Wenn data_store.service "aws_s3" ist, gibt diese Eigenschaft den AWS geheimen Zugangsschlüssel an. Siehe auch data_store.service. Im Protokoll erscheint es als `************`. | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/pfad/zu/backup/dir" | --- | Nur wenn data_store.service = "aws_s3" | Wenn data_store.service "aws_s3" ist, gibt diese Eigenschaft den [Zugangspunkt von AWS S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html) an. Siehe auch data_store.service. | | busy_timeout | 5000 | 5000 | Nein | Gibt den Busy-Timeout für die Datenbank in Millisekunden an. Verhindert sofortige SQLITE_BUSY-Fehler bei Sperrkonflikten. | +| cache_size | 65536 | 65536 | Nein | Gibt die Seiten-Cache-Größe in KiB für die Schreibverbindung der Datenbank an. Ein größerer Cache vermeidet das erneute Lesen verdrängter Seiten bei großen Report-Importen und -Löschungen. Muss größer als 0 sein. | diff --git a/services/console/src/chunks/docs-reference/server-config/en/database.mdx b/services/console/src/chunks/docs-reference/server-config/en/database.mdx index 0deb6c265f..c4f43fd71f 100644 --- a/services/console/src/chunks/docs-reference/server-config/en/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/en/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | Only if data_store.service = "aws_s3" | If data_store.service = "aws_s3", this property specifies the AWS secret access key. See also data_store.service. Whenever logged, it will appear obfuscated as `************`. | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" | --- | Only if data_store.service = "aws_s3" | If data_store.service = "aws_s3", this property specifies the [AWS S3 accesspoint](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html). See also data_store.service. | | busy_timeout | 5000 | 5000 | No | Specifies the busy timeout for the database in milliseconds. Prevents immediate SQLITE_BUSY errors under lock contention. | +| cache_size | 65536 | 65536 | No | Specifies the page cache size in KiB for the writer database connection. A larger cache avoids re-reading evicted pages during large report ingests and deletions. Must be greater than 0. | diff --git a/services/console/src/chunks/docs-reference/server-config/es/database.mdx b/services/console/src/chunks/docs-reference/server-config/es/database.mdx index 549ce327b5..c61ded1c87 100644 --- a/services/console/src/chunks/docs-reference/server-config/es/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/es/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | Solo si data_store.service = "aws_s3" | Si data_store.service = "aws_s3", esta propiedad especifica la clave de acceso secreta de AWS. Ver también data_store.service. Cuando se registra, aparecerá ofuscado como `************`. | | data_store.access_point | "arn:aws:s3:alguna-region-1:123456789:accesspoint/mi-bucket/ruta/hacia/directorio/de/respaldo" | --- | Solo si data_store.service = "aws_s3" | Si data_store.service = "aws_s3", esta propiedad especifica el [punto de acceso S3 de AWS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html). Ver también data_store.service. | | busy_timeout | 5000 | 5000 | No | Especifica el tiempo de espera ocupado para la base de datos en milisegundos. Previene errores SQLITE_BUSY inmediatos bajo contención de bloqueo. | +| cache_size | 65536 | 65536 | No | Especifica el tamaño de la caché de páginas en KiB para la conexión de escritura de la base de datos. Una caché más grande evita releer páginas expulsadas durante ingestas y eliminaciones de informes grandes. Debe ser mayor que 0. | diff --git a/services/console/src/chunks/docs-reference/server-config/example.mdx b/services/console/src/chunks/docs-reference/server-config/example.mdx index dba5135149..20b047335a 100644 --- a/services/console/src/chunks/docs-reference/server-config/example.mdx +++ b/services/console/src/chunks/docs-reference/server-config/example.mdx @@ -32,7 +32,8 @@ "secret_access_key": "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7", "access_point": "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" }, - "busy_timeout": 5000 + "busy_timeout": 5000, + "cache_size": 65536 }, "smtp": { "hostname": "mailbonobo.com", diff --git a/services/console/src/chunks/docs-reference/server-config/fr/database.mdx b/services/console/src/chunks/docs-reference/server-config/fr/database.mdx index a328c62a21..87858955da 100644 --- a/services/console/src/chunks/docs-reference/server-config/fr/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/fr/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | Seulement si data_store.service = "aws_s3" | Si data_store.service = "aws_s3", cette propriété spécifie la clé d'accès secrète AWS. Voir aussi data_store.service. Lorsqu'elle est enregistrée, elle apparaîtra sous forme obfusquée, c'est-à-dire `************`. | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" | --- | Seulement si data_store.service = "aws_s3" | Si data_store.service = "aws_s3", cette propriété spécifie le [point d'accès AWS S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html). Voir aussi data_store.service. | | busy_timeout | 5000 | 5000 | Non | Spécifie le délai d'attente pour la base de données en millisecondes. Empêche les erreurs SQLITE_BUSY immédiates en cas de contention de verrou. | +| cache_size | 65536 | 65536 | Non | Spécifie la taille du cache de pages en KiB pour la connexion d'écriture de la base de données. Un cache plus grand évite de relire les pages évincées lors des ingestions et suppressions de rapports volumineux. Doit être supérieur à 0. | diff --git a/services/console/src/chunks/docs-reference/server-config/ja/database.mdx b/services/console/src/chunks/docs-reference/server-config/ja/database.mdx index 0a3225592d..37cd8a976f 100644 --- a/services/console/src/chunks/docs-reference/server-config/ja/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/ja/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | data_store.service = "aws_s3"の場合のみ | data_store.service = "aws_s3" の場合、このプロパティは AWS シークレットアクセスキーを指定します。 data_store.service も参照してください。ログ時には `************` として表示されます。 | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" | --- | data_store.service = "aws_s3"の場合のみ | data_store.service = "aws_s3" の場合、このプロパティは [AWS S3 アクセスポイント](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html) を指定します。 data_store.service も参照してください。 | | busy_timeout | 5000 | 5000 | いいえ | データベースのビジータイムアウトをミリ秒単位で指定します。ロック競合下での即座のSQLITE_BUSYエラーを防止します。 | +| cache_size | 65536 | 65536 | いいえ | 書き込み用データベース接続のページキャッシュサイズをKiB単位で指定します。キャッシュを大きくすると、大規模なレポートの取り込みや削除の際に追い出されたページの再読み込みを回避できます。 0より大きい値でなければなりません。 | diff --git a/services/console/src/chunks/docs-reference/server-config/ko/database.mdx b/services/console/src/chunks/docs-reference/server-config/ko/database.mdx index ded3c9d599..937933a8cb 100644 --- a/services/console/src/chunks/docs-reference/server-config/ko/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/ko/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | data_store.service = "aws_s3"인 경우만 | data_store.service = "aws_s3"인 경우, 이 속성은 AWS secret access key를 지정합니다. data_store.service에 대해서도 참조하십시오. 로그에 기록될 때 `************`로 표현됩니다. | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" | --- | data_store.service = "aws_s3"인 경우만 | data_store.service = "aws_s3"인 경우, 이 속성은 [AWS S3 accesspoint](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html)를 지정합니다. data_store.service에 대해서도 참조하십시오. | | busy_timeout | 5000 | 5000 | 아니오 | 데이터베이스의 busy 타임아웃을 밀리초(ms) 단위로 지정합니다. 잠금 경합 시 즉각적인 SQLITE_BUSY 오류를 방지합니다. | +| cache_size | 65536 | 65536 | 아니오 | 쓰기 데이터베이스 연결의 페이지 캐시 크기를 KiB 단위로 지정합니다. 캐시가 클수록 대규모 리포트 수집 및 삭제 시 축출된 페이지를 다시 읽는 것을 방지합니다. 0보다 커야 합니다. | diff --git a/services/console/src/chunks/docs-reference/server-config/pt/database.mdx b/services/console/src/chunks/docs-reference/server-config/pt/database.mdx index 36305ec25f..130e1403d5 100644 --- a/services/console/src/chunks/docs-reference/server-config/pt/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/pt/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | Apenas se data_store.service = "aws_s3" | Se data_store.service = "aws_s3", esta propriedade especifica a chave de acesso secreta do AWS. Veja também data_store.service. Quando registrada, ela aparecerá obfuscada como `************`. | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" | --- | Apenas se data_store.service = "aws_s3" | Se data_store.service = "aws_s3", esta propriedade especifica o [ponto de acesso do AWS S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html). Veja também data_store.service. | | busy_timeout | 5000 | 5000 | Não | Especifica o tempo limite de ocupação para o banco de dados em milissegundos. Previne erros SQLITE_BUSY imediatos sob contenção de bloqueio. | +| cache_size | 65536 | 65536 | Não | Especifica o tamanho do cache de páginas em KiB para a conexão de escrita do banco de dados. Um cache maior evita a releitura de páginas removidas durante ingestões e exclusões de relatórios grandes. Deve ser maior que 0. | diff --git a/services/console/src/chunks/docs-reference/server-config/ru/database.mdx b/services/console/src/chunks/docs-reference/server-config/ru/database.mdx index 7a38754a51..15b3983d13 100644 --- a/services/console/src/chunks/docs-reference/server-config/ru/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/ru/database.mdx @@ -8,4 +8,5 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | Только если data_store.service = "aws_s3" | Если data_store.service = "aws_s3", это свойство указывает секретный ключ доступа AWS. Смотрите также data_store.service. При логировании он будет обфусцирован как `************`. | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" | --- | Только если data_store.service = "aws_s3" | Если data_store.service = "aws_s3", это свойство указывает [точку доступа AWS S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html). Смотрите также data_store.service. | | busy_timeout | 5000 | 5000 | Нет | Указывает тайм-аут занятости для базы данных в миллисекундах. Предотвращает немедленные ошибки SQLITE_BUSY при блокировке. | +| cache_size | 65536 | 65536 | Нет | Указывает размер страничного кэша в КиБ для соединения записи базы данных. Больший кэш позволяет избежать повторного чтения вытесненных страниц при больших загрузках и удалениях отчетов. Должен быть больше 0. | diff --git a/services/console/src/chunks/docs-reference/server-config/zh/database.mdx b/services/console/src/chunks/docs-reference/server-config/zh/database.mdx index 3138ffe409..7c48116de5 100644 --- a/services/console/src/chunks/docs-reference/server-config/zh/database.mdx +++ b/services/console/src/chunks/docs-reference/server-config/zh/database.mdx @@ -8,3 +8,4 @@ | data_store.secret_access_key | "AA3Chr-JSF5sUQqKwayx-FvCfZKsMev-5BqPpcFC3m7" | --- | 仅在 data_store.service = "aws_s3"时 | 如果 data_store.service = "aws_s3",此属性指定 AWS 私密访问密钥。参见 data_store.service。当进行记录时,它将显示为 `************`。 | | data_store.access_point | "arn:aws:s3:some-region-1:123456789:accesspoint/my-bucket/path/to/backup/dir" | --- | 仅在 data_store.service = "aws_s3"时 | 如果 data_store.service 是 "aws_s3",此属性指定 [AWS S3 接入点](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html)。参见 data_store.service。 | | busy_timeout | 5000 | 5000 | 否 | 指定数据库的忙碌超时时间,以毫秒为单位。防止在锁争用时立即出现SQLITE_BUSY错误。 | +| cache_size | 65536 | 65536 | 否 | 指定写入数据库连接的页面缓存大小,以KiB为单位。更大的缓存可以避免在大型报告摄取和删除期间重新读取被逐出的页面。 必须大于0。 | diff --git a/services/console/src/content/docs-reference/de/changelog.mdx b/services/console/src/content/docs-reference/de/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/de/changelog.mdx +++ b/services/console/src/content/docs-reference/de/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/de/server-config.mdx b/services/console/src/content/docs-reference/de/server-config.mdx index 020d75acee..ba0bb3da8b 100644 --- a/services/console/src/content/docs-reference/de/server-config.mdx +++ b/services/console/src/content/docs-reference/de/server-config.mdx @@ -3,7 +3,7 @@ title: "Server-Konfiguration" description: "Bencher API-Server-Konfiguration für kontinuierliches Benchmarking im Eigenbetrieb" heading: "API-Server-Konfiguration" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/en/changelog.mdx b/services/console/src/content/docs-reference/en/changelog.mdx index 784f835689..14394c2d53 100644 --- a/services/console/src/content/docs-reference/en/changelog.mdx +++ b/services/console/src/content/docs-reference/en/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher release changelog" heading: "Bencher Changelog" published: "2023-08-12T16:07:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 --- diff --git a/services/console/src/content/docs-reference/en/server-config.mdx b/services/console/src/content/docs-reference/en/server-config.mdx index dfa10d1007..e047a9130e 100644 --- a/services/console/src/content/docs-reference/en/server-config.mdx +++ b/services/console/src/content/docs-reference/en/server-config.mdx @@ -3,7 +3,7 @@ title: "API Server Config" description: "Bencher API server configuration for self-hosted continuous benchmarking" heading: "API Server Config" published: "2023-08-12T16:07:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/es/changelog.mdx b/services/console/src/content/docs-reference/es/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/es/changelog.mdx +++ b/services/console/src/content/docs-reference/es/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/es/server-config.mdx b/services/console/src/content/docs-reference/es/server-config.mdx index 7301319648..ecad8aa464 100644 --- a/services/console/src/content/docs-reference/es/server-config.mdx +++ b/services/console/src/content/docs-reference/es/server-config.mdx @@ -3,7 +3,7 @@ title: "Configuración del Servidor" description: "Configuración del servidor API de Bencher para benchmarking continuo autoalojado" heading: "Configuración del Servidor API" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/fr/changelog.mdx b/services/console/src/content/docs-reference/fr/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/fr/changelog.mdx +++ b/services/console/src/content/docs-reference/fr/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/fr/server-config.mdx b/services/console/src/content/docs-reference/fr/server-config.mdx index 0fdc61168f..396f47826c 100644 --- a/services/console/src/content/docs-reference/fr/server-config.mdx +++ b/services/console/src/content/docs-reference/fr/server-config.mdx @@ -3,7 +3,7 @@ title: "Config Serveur" description: "Configuration du serveur API Bencher pour le benchmarking continu auto-hébergé" heading: "Config Serveur API" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/ja/changelog.mdx b/services/console/src/content/docs-reference/ja/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/ja/changelog.mdx +++ b/services/console/src/content/docs-reference/ja/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/ja/server-config.mdx b/services/console/src/content/docs-reference/ja/server-config.mdx index 4454780819..369c25f19b 100644 --- a/services/console/src/content/docs-reference/ja/server-config.mdx +++ b/services/console/src/content/docs-reference/ja/server-config.mdx @@ -3,7 +3,7 @@ title: "サーバー設定" description: "自己ホスト型の継続的なベンチマーキングのためのBencher APIサーバー設定" heading: "APIサーバー設定" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/ko/changelog.mdx b/services/console/src/content/docs-reference/ko/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/ko/changelog.mdx +++ b/services/console/src/content/docs-reference/ko/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/ko/server-config.mdx b/services/console/src/content/docs-reference/ko/server-config.mdx index 17f7d2b34a..05cbca83dd 100644 --- a/services/console/src/content/docs-reference/ko/server-config.mdx +++ b/services/console/src/content/docs-reference/ko/server-config.mdx @@ -3,7 +3,7 @@ title: "서버 설정" description: "자체 호스팅을 위한 지속적인 벤치마킹을 위한 Bencher API 서버 구성" heading: "API 서버 설정" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/pt/changelog.mdx b/services/console/src/content/docs-reference/pt/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/pt/changelog.mdx +++ b/services/console/src/content/docs-reference/pt/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/pt/server-config.mdx b/services/console/src/content/docs-reference/pt/server-config.mdx index 82d3f277bb..bfa79afd9a 100644 --- a/services/console/src/content/docs-reference/pt/server-config.mdx +++ b/services/console/src/content/docs-reference/pt/server-config.mdx @@ -3,7 +3,7 @@ title: "Configuração do Servidor" description: "Configuração do servidor da API Bencher para benchmarking contínuo auto-hospedado" heading: "Configuração do Servidor API" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/ru/changelog.mdx b/services/console/src/content/docs-reference/ru/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/ru/changelog.mdx +++ b/services/console/src/content/docs-reference/ru/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/ru/server-config.mdx b/services/console/src/content/docs-reference/ru/server-config.mdx index 0467c84d85..d3a0ea4653 100644 --- a/services/console/src/content/docs-reference/ru/server-config.mdx +++ b/services/console/src/content/docs-reference/ru/server-config.mdx @@ -3,7 +3,7 @@ title: "Конфигурация сервера" description: "Конфигурация сервера API Bencher для самостоятельного непрерывного бенчмаркинга" heading: "Конфигурация API сервера" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 --- diff --git a/services/console/src/content/docs-reference/zh/changelog.mdx b/services/console/src/content/docs-reference/zh/changelog.mdx index 06c043d914..42422c8756 100644 --- a/services/console/src/content/docs-reference/zh/changelog.mdx +++ b/services/console/src/content/docs-reference/zh/changelog.mdx @@ -3,7 +3,7 @@ title: "Changelog" description: "Bencher changelog" heading: "Bencher Changelog" published: "2023-10-27T08:40:00Z" -modified: "2026-06-19T08:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 10 canonicalize: true --- diff --git a/services/console/src/content/docs-reference/zh/server-config.mdx b/services/console/src/content/docs-reference/zh/server-config.mdx index 8bd554505f..02a98b04ce 100644 --- a/services/console/src/content/docs-reference/zh/server-config.mdx +++ b/services/console/src/content/docs-reference/zh/server-config.mdx @@ -3,7 +3,7 @@ title: "服务器配置" description: "为自托管的持续基准测试配置 Bencher API 服务器" heading: "API 服务器配置" published: "2023-10-27T08:40:00Z" -modified: "2026-06-18T00:00:00Z" +modified: "2026-07-08T00:00:00Z" sortOrder: 7 ---