From 7e34f8947d8a6fc97b382720964d437e018d80e8 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sat, 25 Apr 2026 18:15:05 -0400 Subject: [PATCH 01/12] add benchmarks website v3 design overview and plan Signed-off-by: Connor Tsui --- benchmarks-website/planning/00-overview.md | 103 ++++++++ benchmarks-website/planning/01-schema.md | 222 ++++++++++++++++++ benchmarks-website/planning/02-contracts.md | 195 +++++++++++++++ benchmarks-website/planning/AGENTS.md | 86 +++++++ benchmarks-website/planning/README.md | 69 ++++++ .../planning/benchmark-mapping.md | 147 ++++++++++++ .../planning/components/emitter.md | 86 +++++++ .../planning/components/server.md | 70 ++++++ .../planning/components/web-ui.md | 62 +++++ benchmarks-website/planning/decisions.md | 90 +++++++ benchmarks-website/planning/deferred.md | 118 ++++++++++ 11 files changed, 1248 insertions(+) create mode 100644 benchmarks-website/planning/00-overview.md create mode 100644 benchmarks-website/planning/01-schema.md create mode 100644 benchmarks-website/planning/02-contracts.md create mode 100644 benchmarks-website/planning/AGENTS.md create mode 100644 benchmarks-website/planning/README.md create mode 100644 benchmarks-website/planning/benchmark-mapping.md create mode 100644 benchmarks-website/planning/components/emitter.md create mode 100644 benchmarks-website/planning/components/server.md create mode 100644 benchmarks-website/planning/components/web-ui.md create mode 100644 benchmarks-website/planning/decisions.md create mode 100644 benchmarks-website/planning/deferred.md diff --git a/benchmarks-website/planning/00-overview.md b/benchmarks-website/planning/00-overview.md new file mode 100644 index 00000000000..0fcb9557aab --- /dev/null +++ b/benchmarks-website/planning/00-overview.md @@ -0,0 +1,103 @@ + + +# 00 - Overview + +## What we're building + +A replacement for the current `bench.vortex.dev` site. The new +stack is a **single Rust binary** that owns a **DuckDB database** +on local disk and serves the website plus an `/api/ingest` route. +CI eventually POSTs new benchmark results there. There is no +separate ingester service, no S3 coordination layer for writes, no +client-side WASM. + +HTTP framework, templating engine, and module layout are the +server agent's call. + +## Phasing + +We build this in two phases. **Plan only the first.** + +### Alpha (this plan) + +The smallest end-to-end loop that proves the design: + +1. **Schema** locked enough to ingest one benchmark result. +2. **Server**: open DuckDB, accept a bearer-token-authenticated POST, + serve a couple of read routes. +3. **Emitter**: `vortex-bench --gh-json-v3` + a tiny POST script. +4. **Web UI**: one landing page + one chart page rendered against a + fixture DB. + +That's it. No production deploy, no historical data import, no CI +workflow integration, no admin tooling, no schema migration +framework, no auth beyond the shared bearer token. All of those +live in [`deferred.md`](./deferred.md). + +The alpha runs on a developer machine. v2 keeps running in +production unchanged. There is no cutover in alpha. + +### Phase 2 and beyond + +Once the alpha loop is green, we layer in production deploy, +historical migration, CI dual-write, and the rest of the v2-parity +work. Stubs are in [`deferred.md`](./deferred.md). + +## Architecture (alpha) + +One process, one DB file. The server is the API and the website. +The emitter writes JSONL of bare records; a small POST script +wraps and uploads them. CI isn't wired up yet; ingest happens +manually during alpha. + +## Components + +Three components for alpha. Each is one workstream, one branch, one +PR. + +| Component | Plan | Owns | +|---|---|---| +| Server | [components/server.md](./components/server.md) | DuckDB open + schema, bearer-auth ingest, read routes, HTML routes mounted from web-ui | +| Emitter | [components/emitter.md](./components/emitter.md) | `vortex-bench --gh-json-v3` + the post-ingest script | +| Web UI | [components/web-ui.md](./components/web-ui.md) | Landing page + chart page, against a fixture DuckDB | + +### Dependencies + +The schema feeds all three components. The contracts feed the +server and the emitter. With both stable, **all three components +can be worked on in parallel**. + +## Goals + +In priority order: + +1. **End-to-end alpha loop works.** Emit → POST → store → render. +2. **Schema is the right shape.** Five fact tables (one per + measurement family) plus a `commits` dim. See + [`01-schema.md`](./01-schema.md). +3. **Each component is small enough that one agent can finish it + in one PR.** No mega-PRs. + +Cutover, parity, and "faster than v2" are explicit non-goals at +alpha; they come back in phase 2. + +## Shared docs + +- [`00-overview.md`](./00-overview.md) (this file) +- [`01-schema.md`](./01-schema.md) - the five fact tables + `commits` +- [`02-contracts.md`](./02-contracts.md) - wire shapes + HTTP error + matrix + auth header +- [`benchmark-mapping.md`](./benchmark-mapping.md) - existing + benchmarks → fact tables +- [`decisions.md`](./decisions.md) - resolved decisions +- [`deferred.md`](./deferred.md) - phase-2 stubs + +## Status of v2 during alpha + +v2 stays in production untouched. Do not edit +`benchmarks-website/server.js` or `benchmarks-website/src/`. v3 +lives alongside under `benchmarks-website/` in a new Cargo crate +(path is the server agent's call). diff --git a/benchmarks-website/planning/01-schema.md b/benchmarks-website/planning/01-schema.md new file mode 100644 index 00000000000..8ddf71e2739 --- /dev/null +++ b/benchmarks-website/planning/01-schema.md @@ -0,0 +1,222 @@ + + +# 01 - DuckDB schema (alpha) + +The persistent data model. **One `commits` dim table plus five fact +tables, one per measurement family.** No lookup tables, no views, no +migration framework; those are deferred (see +[`deferred.md`](./deferred.md)). + +## Design principles + +1. **One fact table per (dim shape, value shape).** A row in any + fact table has every value column populated; NULLs only appear + in genuinely optional dimensions. +2. **No discriminator columns spanning families.** No `metric_kind` + enum forcing five shapes into one row. +3. **No JSON escape hatch.** New benchmark parameters become real + columns. Adding a nullable column is cheap; the readability win + is worth it. +4. **Hashed primary key per table.** Each fact table has a + `measurement_id` that is a deterministic 64-bit hash of that + table's dimensional tuple. Server-internal; not on the wire. +5. **`commits` is the only dim table.** Engine, format, dataset, + etc. stay as inline strings; DuckDB's dictionary encoding makes + a lookup table pointless. +6. **Ratios are not stored.** Computed at query time from + `compression_sizes`. + +## Why five fact tables, not one + +The five families have genuinely different shapes: + +| Table | Shape sketch | +|---|---| +| `query_measurements` | dataset + query_idx + engine + format + storage → timing **and** memory | +| `compression_times` | dataset + format + op∈{encode,decode} → timing | +| `compression_sizes` | dataset + format → bytes | +| `random_access_times` | dataset + format → timing (different dataset namespace) | +| `vector_search_runs` | dataset + layout + flavor + threshold → timing + counters | + +Forcing them into one table either bloats every row with columns +that are NULL for ~99% of rows (`layout`, `flavor`, `threshold`, +`matches`, `rows_scanned`, `bytes_scanned`) or splits scan results +across multiple rows that have to be re-joined to render one chart. + +## Group / chart / series fit + +The render-time view used by `/api/groups` and `/api/chart/:slug` +is mechanically derivable per table: + +| Table | Group key | Chart key | Series key | +|---|---|---|---| +| `query_measurements` | `(dataset, dataset_variant, scale_factor, storage)` | `(dataset, query_idx)` | `(engine, format)` | +| `compression_times` | constant `"Compression"` | `(dataset, dataset_variant)` | `(format, op)` | +| `compression_sizes` | constant `"Compression Size"` | `(dataset, dataset_variant)` | `format` | +| `random_access_times` | constant `"Random Access"` | `dataset` | `format` | +| `vector_search_runs` | `(dataset, layout)` | `(dataset, layout, threshold)` | `flavor` | + +The classifier logic in v2's `v2-classifier.js` mostly disappears - +each table already knows what suite it represents. + +## Tables + +DDL is the server's call. Below is the column contract: name, type +family, and whether it's NOT NULL. The server agent picks exact +DuckDB types, indexes, and constraint syntax. + +### `commits` (dim) + +| Column | Type | Required? | Notes | +|---|---|---|---| +| `commit_sha` | string | yes (PK) | 40-hex lowercase | +| `timestamp` | timestamptz | yes | | +| `message` | string | yes | first line only | +| `author_name` | string | yes | | +| `author_email` | string | yes | | +| `committer_name` | string | yes | | +| `committer_email` | string | yes | | +| `tree_sha` | string | yes | | +| `url` | string | yes | | + +Populated from the envelope on every `/api/ingest` call. + +### `query_measurements` + +SQL query suites: TPC-H, TPC-DS, ClickBench, StatPopGen, +PolarSignals, Fineweb, GhArchive, Public-BI. Memory columns are +populated when the run was instrumented for memory; NULL otherwise. +Timing and memory share the row because they're produced together +for the same query execution. + +| Column | Type | Required? | Notes | +|---|---|---|---| +| `measurement_id` | int64 | yes (PK) | hash of dim tuple | +| `commit_sha` | string | yes | FK to `commits` | +| `dataset` | string | yes | `tpch`, `tpcds`, `clickbench`, ... | +| `dataset_variant` | string | optional | ClickBench flavor, Public-BI name | +| `scale_factor` | string | optional | TPC SF; n_rows for StatPopGen / PolarSignals | +| `query_idx` | int32 | yes | 1-based | +| `storage` | string | yes | `nvme` or `s3` | +| `engine` | string | yes | `datafusion`, `duckdb`, `vortex`, `arrow` | +| `format` | string | yes | `vortex-file-compressed`, `parquet`, `lance`, ... | +| `value_ns` | int64 | yes | median timing, ns | +| `all_runtimes_ns` | list<int64> | yes | per-iteration timings | +| `peak_physical` | int64 | optional | bytes | +| `peak_virtual` | int64 | optional | bytes | +| `physical_delta` | int64 | optional | bytes | +| `virtual_delta` | int64 | optional | bytes | +| `env_triple` | string | optional | e.g. `x86_64-linux-gnu` | + +### `compression_times` + +Encode/decode timings from `compress-bench`. + +| Column | Type | Required? | Notes | +|---|---|---|---| +| `measurement_id` | int64 | yes (PK) | | +| `commit_sha` | string | yes | FK | +| `dataset` | string | yes | | +| `dataset_variant` | string | optional | | +| `format` | string | yes | | +| `op` | string | yes | `encode` or `decode` | +| `value_ns` | int64 | yes | | +| `all_runtimes_ns` | list<int64> | yes | | +| `env_triple` | string | optional | | + +### `compression_sizes` + +On-disk sizes from `compress-bench`. One-shot, no per-iteration data. +Compression ratios in v2 (`vortex:parquet-zstd ratio/...`) are a +SELECT over this table joined to itself; they're not stored. + +| Column | Type | Required? | Notes | +|---|---|---|---| +| `measurement_id` | int64 | yes (PK) | | +| `commit_sha` | string | yes | FK | +| `dataset` | string | yes | | +| `dataset_variant` | string | optional | | +| `format` | string | yes | | +| `value_bytes` | int64 | yes | | + +### `random_access_times` + +Take-time timings from `random-access-bench`. Different dataset +namespace from `compression_times` - kept in its own table so +dataset filters never have to disambiguate which suite a row +belongs to. + +| Column | Type | Required? | Notes | +|---|---|---|---| +| `measurement_id` | int64 | yes (PK) | | +| `commit_sha` | string | yes | FK | +| `dataset` | string | yes | | +| `format` | string | yes | | +| `value_ns` | int64 | yes | | +| `all_runtimes_ns` | list<int64> | yes | | +| `env_triple` | string | optional | | + +### `vector_search_runs` + +Cosine-similarity scans from `vector-search-bench`. The only family +that emits a timing **plus side counters** for the same scan; +keeping them in one row avoids a 1:N split that has to be re-joined +on read. + +| Column | Type | Required? | Notes | +|---|---|---|---| +| `measurement_id` | int64 | yes (PK) | | +| `commit_sha` | string | yes | FK | +| `dataset` | string | yes | e.g. `cohere-large-10m` | +| `layout` | string | yes | `TrainLayout`, e.g. `partitioned` | +| `flavor` | string | yes | `VectorFlavor`, e.g. `vortex-turboquant` | +| `threshold` | double | yes | cosine threshold | +| `value_ns` | int64 | yes | per-scan wall time | +| `all_runtimes_ns` | list<int64> | yes | | +| `matches` | int64 | yes | | +| `rows_scanned` | int64 | yes | | +| `bytes_scanned` | int64 | yes | | +| `iterations` | int32 | yes | not part of the dim hash | +| `env_triple` | string | optional | | + +## `measurement_id` hash + +Per-table xxhash64 over each table's dimensional tuple. The hash is +**server-internal** - the wire never carries it. The server's INSERT +path computes it before each `INSERT ... ON CONFLICT DO UPDATE`, +which gives idempotent upsert on re-emission of the same dim tuple. +Encoding details (input order, NULL handling, byte layout) are the +server's call, since the value never crosses a process boundary. + +When the historical migrator lands (deferred), it reuses the +server's hash function via a shared crate. + +## Storage values + +`storage` is `'nvme'` or `'s3'`. Legacy `gcs` is dropped. Only +`query_measurements` carries `storage` - the other families don't +fan out by storage backend. + +## Schema changes during alpha + +There is no migration framework. If you change the schema: + +1. Update this doc. +2. Update the server's DDL. +3. Delete any local `bench.duckdb` and re-run. + +A real forward-only migration framework lands post-alpha. See +[`deferred.md`](./deferred.md). + +## What's intentionally NOT here (deferred) + +- `schema_meta` and migration framework. +- `known_engines` / `known_formats` / `known_datasets` lookup + tables and seed SQL. +- Views (`v_compression_ratios`, `v_latest_per_group`, etc.). +- Pre-downsampled aliases. +- A `microbench_runs` table - reserved as the next family to add + when microbench results start landing. diff --git a/benchmarks-website/planning/02-contracts.md b/benchmarks-website/planning/02-contracts.md new file mode 100644 index 00000000000..9aba31fb73b --- /dev/null +++ b/benchmarks-website/planning/02-contracts.md @@ -0,0 +1,195 @@ + + +# 02 - Wire contracts (alpha) + +The cross-component glue between the emitter, the POST script, and +the server. Wire-format only - implementations are local to each +component. + +If two components disagree about a shape, **this file is right** +and both update. + +## Records are discriminated by `kind` + +Each record on the wire carries a `kind` field that picks one of +the [five fact tables](./01-schema.md#tables). The emitter never +decides "what column" - it decides "what kind", and the rest of the +row is that kind's flat field set. + +| `kind` | Destination table | +|---|---| +| `query_measurement` | `query_measurements` | +| `compression_time` | `compression_times` | +| `compression_size` | `compression_sizes` | +| `random_access_time` | `random_access_times` | +| `vector_search_run` | `vector_search_runs` | + +**Unknown `kind` values cause a 400.** Unknown fields within a known +`kind` also cause a 400. Version skew should fail loudly. + +## Per-kind record shapes + +All shared metadata first; per-kind fields after. + +### `query_measurement` + +| Field | Type | Required? | Notes | +|---|---|---|---| +| `kind` | `"query_measurement"` | yes | discriminator | +| `commit_sha` | string | yes | 40-hex lowercase | +| `dataset` | string | yes | `tpch`, `tpcds`, `clickbench`, ... | +| `dataset_variant` | string | optional | ClickBench flavor, Public-BI name | +| `scale_factor` | string | optional | TPC SF; n_rows for StatPopGen / PolarSignals | +| `query_idx` | integer | yes | 1-based | +| `storage` | enum string | yes | `nvme` or `s3` | +| `engine` | string | yes | `datafusion`, `duckdb`, `vortex`, `arrow` | +| `format` | string | yes | `vortex-file-compressed`, `parquet`, `lance`, ... | +| `value_ns` | integer | yes | median timing, ns | +| `all_runtimes_ns` | array<integer> | yes | per-iteration timings (may be empty) | +| `peak_physical` | integer | optional | bytes | +| `peak_virtual` | integer | optional | bytes | +| `physical_delta` | integer | optional | bytes | +| `virtual_delta` | integer | optional | bytes | +| `env_triple` | string | optional | e.g. `x86_64-linux-gnu` | + +The four memory fields are populated together (all four or none). + +### `compression_time` + +| Field | Type | Required? | Notes | +|---|---|---|---| +| `kind` | `"compression_time"` | yes | | +| `commit_sha` | string | yes | | +| `dataset` | string | yes | | +| `dataset_variant` | string | optional | | +| `format` | string | yes | | +| `op` | enum string | yes | `encode` or `decode` | +| `value_ns` | integer | yes | | +| `all_runtimes_ns` | array<integer> | yes | | +| `env_triple` | string | optional | | + +### `compression_size` + +| Field | Type | Required? | Notes | +|---|---|---|---| +| `kind` | `"compression_size"` | yes | | +| `commit_sha` | string | yes | | +| `dataset` | string | yes | | +| `dataset_variant` | string | optional | | +| `format` | string | yes | | +| `value_bytes` | integer | yes | | + +### `random_access_time` + +| Field | Type | Required? | Notes | +|---|---|---|---| +| `kind` | `"random_access_time"` | yes | | +| `commit_sha` | string | yes | | +| `dataset` | string | yes | random-access dataset name (e.g. `chimp`, `taxi`) | +| `format` | string | yes | | +| `value_ns` | integer | yes | | +| `all_runtimes_ns` | array<integer> | yes | | +| `env_triple` | string | optional | | + +### `vector_search_run` + +| Field | Type | Required? | Notes | +|---|---|---|---| +| `kind` | `"vector_search_run"` | yes | | +| `commit_sha` | string | yes | | +| `dataset` | string | yes | e.g. `cohere-large-10m` | +| `layout` | string | yes | `TrainLayout`, e.g. `partitioned` | +| `flavor` | string | yes | `VectorFlavor`, e.g. `vortex-turboquant` | +| `threshold` | number | yes | cosine threshold | +| `value_ns` | integer | yes | per-scan wall time (median of iterations) | +| `all_runtimes_ns` | array<integer> | yes | | +| `matches` | integer | yes | | +| `rows_scanned` | integer | yes | | +| `bytes_scanned` | integer | yes | | +| `iterations` | integer | yes | | +| `env_triple` | string | optional | | + +## Ingest envelope + +`/api/ingest` accepts one envelope per POST. The envelope wraps a +heterogeneous batch of records (any mix of `kind`s). Required +top-level fields: + +- `run_meta`: object with `benchmark_id` (string), `schema_version` + (integer; `1` at alpha), `started_at` (RFC 3339 timestamp). +- `commit`: object with the columns of the [`commits` + table](./01-schema.md#commits-dim), keyed by their column names + with `commit_sha` renamed to `sha`. The server upserts this row + before applying records. +- `records`: array of per-`kind` records as defined above. + +`vortex-bench --gh-json-v3 ` writes JSONL of bare records +only. The envelope (`run_meta` + `commit`) is added by the +post-ingest script before POSTing - this keeps the Rust emitter +dependency-light. + +The post-ingest script is responsible for filling the `commit` +fields. CI has the SHA from `${{ github.sha }}`; the rest comes +from `git show` or equivalent. See +[`components/emitter.md`](./components/emitter.md). + +## HTTP matrix for `POST /api/ingest` + +| Condition | Status | +|---|---| +| Happy path | 200 with `{ "inserted": N, "updated": M }` | +| Malformed JSON | 400 | +| Unknown `kind`, unknown field, or per-record validation failure | 400 with the offending record index | +| Missing/invalid bearer token | 401 | +| Schema version newer than server expects | 409 | +| Other server error | 500 | + +All-or-nothing per POST: a single failed record fails the whole +batch. The reported `inserted` and `updated` counts are aggregated +across all five tables. + +## Authentication header + +```text +Authorization: Bearer +``` + +Compared with constant-time equality on the server. Token comes from +the `INGEST_BEARER_TOKEN` env var. + +## Slug grammar (server ↔ web-ui) + +The web-ui receives slugs from `/api/groups` and feeds them back +into `/api/chart/:slug`. Slugs are **opaque strings** as far as the +web-ui is concerned: it never parses or constructs them itself, +only echoes what the API returned. The server is free to choose any +slug format, change it without breaking the web-ui, or make it +debuggable (e.g. `qm-tpch-q01-nvme-sf1`) - the only contract is +"`/api/chart/:slug` accepts any slug `/api/groups` returned." + +## Read API (alpha) + +Two routes - just enough to render one chart page. Field shapes are +not binding; refine during implementation. + +### `GET /api/groups` + +A flat list of distinct group keys derivable from the data, with +just enough metadata to link to a chart. The server walks each fact +table to produce the group keys defined in +[`01-schema.md`](./01-schema.md#group--chart--series-fit). Every +chart entry includes a `slug` that round-trips through +`/api/chart/:slug`. + +### `GET /api/chart/:slug` + +Returns the data for one chart: a `display_name`, a `unit`, an +ordered `commits` list (sha + timestamp + first-line message + url), +and a `series` map keyed by series name where each value is an +array aligned to `commits` (with `null` for missing data points). + +Per-commit page, zoom/pan, range queries, and the rest of the read +API are deferred. See [`deferred.md`](./deferred.md). diff --git a/benchmarks-website/planning/AGENTS.md b/benchmarks-website/planning/AGENTS.md new file mode 100644 index 00000000000..fe5cb69894e --- /dev/null +++ b/benchmarks-website/planning/AGENTS.md @@ -0,0 +1,86 @@ + + +# AGENTS.md - benchmarks-website v3 (alpha) + +Brief for coding agents working on this rewrite. Keep it short; +detail belongs in component plans. + +## What you're working on + +The **alpha** of v3 of `bench.vortex.dev`. Target: a single Rust +binary with **DuckDB on local disk**. The smallest end-to-end loop +that proves the design. + +The v2 site at `benchmarks-website/` is in production and stays +running unchanged. v3 lives alongside in a new crate under +`benchmarks-website/` (path is the server agent's call). + +Anything not listed in [`README.md`](./README.md) under +"Components" is **deferred**. See [`deferred.md`](./deferred.md). +Don't expand scope past your component plan. + +## Where to start + +1. [`README.md`](./README.md) - reading order. +2. [`00-overview.md`](./00-overview.md) - phases, components, + dependency map. +3. [`01-schema.md`](./01-schema.md) - the DuckDB schema (column + contracts; SQL is the server agent's call). +4. [`02-contracts.md`](./02-contracts.md) - wire shapes + HTTP + matrix + auth header. +5. [`benchmark-mapping.md`](./benchmark-mapping.md) - existing + benchmarks → fact tables (read this if you're working on the + emitter or eventual migration). +6. Your component plan in [`components/`](./components/). + +You **don't** need to read other components' plans. + +## Repository conventions + +See the root [`CLAUDE.md`](/CLAUDE.md) for Rust style, test layout, +and CI norms. Project-specific: + +- New crates go under `benchmarks-website/`. Add to root + `Cargo.toml` workspace members. +- All commits need a `Signed-off-by:` trailer. +- Run `cargo +nightly fmt --all` and narrow clippy on what you + changed. +- Public-API changes need `./scripts/public-api.sh`. +- Every new public item needs a doc comment. +- Tests return `VortexResult<()>` and use `?`. No `unwrap`. + +## Things to avoid + +- **Don't widen scope past your component plan.** If a feature + feels missing, check [`deferred.md`](./deferred.md) first - it + is almost certainly already deferred there. +- **Don't write a server-side classifier.** The emitter is + responsible for v3-shape records. +- **Don't drift from contracts.** Wire-shape changes are a + coordinated PR across the affected components. +- **Don't touch the v2 React/Node app.** It stays in production + unchanged through alpha and through phase 2 until cutover. +- **Don't reach for WASM.** + +## Working branches + +| Branch | Purpose | +|---|---| +| `develop` | Live v2 site. Don't break. | +| `claude/review-benchmarks-redesign-BO3la` | This planning branch. | +| `claude/benchmarks-v3-` | Per-workstream feature branches. | + +Component branches start from `develop`. + +## How to update this file + +Keep it short. If you've learned something a future agent will need: + +- Cross-component contract → [`02-contracts.md`](./02-contracts.md) +- Local detail → your component plan +- Decided → [`decisions.md`](./decisions.md) +- Not designing yet → [`deferred.md`](./deferred.md) +- Cross-cutting agent norm → here diff --git a/benchmarks-website/planning/README.md b/benchmarks-website/planning/README.md new file mode 100644 index 00000000000..34a379b8bd3 --- /dev/null +++ b/benchmarks-website/planning/README.md @@ -0,0 +1,69 @@ + + +# Benchmarks website v3 - Planning + +Planning docs for rebuilding `bench.vortex.dev` as a single Rust +binary with DuckDB on local disk. + +This plan is **alpha-only**. Everything beyond the smallest +end-to-end loop is deliberately punted to +[`deferred.md`](./deferred.md). + +## Reading order + +| File | Read when | +|---|---| +| [`00-overview.md`](./00-overview.md) | Always. The pitch, phases, and dependency map. | +| [`01-schema.md`](./01-schema.md) | Always. The five DuckDB fact tables + `commits` dim. | +| [`02-contracts.md`](./02-contracts.md) | Always. Wire shapes (one `kind` per fact table), HTTP error matrix, auth header. | +| [`benchmark-mapping.md`](./benchmark-mapping.md) | Always when working on the emitter or the historical migrator. Maps every existing benchmark to its target table. | +| [`decisions.md`](./decisions.md) | Skim once. What's pinned for alpha. | +| [`deferred.md`](./deferred.md) | Skim once. What we're not designing yet. | +| `components/.md` | The plan for your specific workstream. | +| `components/.md` | Avoid. If you're tempted, `02-contracts.md` probably needs an update. | + +## Components + +Three components for alpha. Each is one workstream, one branch, one +PR. After the schema and contracts are stable, **all three can be +worked on in parallel**. + +| Component | Plan | Branch | +|---|---|---| +| Server | [components/server.md](./components/server.md) | `claude/benchmarks-v3-server` | +| Emitter | [components/emitter.md](./components/emitter.md) | `claude/benchmarks-v3-emitter` | +| Web UI | [components/web-ui.md](./components/web-ui.md) | `claude/benchmarks-v3-web-ui` | + +## Working branches + +- `develop` - the v2 site, in production. **Do not touch.** +- `claude/review-benchmarks-redesign-BO3la` - this planning branch. +- Component branches above - one per workstream, branched from + `develop`. + +## What this plan is not + +- Not implementation instructions. Component plans are deliberately + high-level. +- Not a phase-2 plan. Phase-2 work is one paragraph each in + [`deferred.md`](./deferred.md). The path will be clearer once the + alpha loop is running. +- Not a parity-with-v2 plan. v2 keeps running unchanged through + alpha. + +## Updating these docs + +If you find a gap, prefer to: + +1. Update [`02-contracts.md`](./02-contracts.md) when the gap is at + a component boundary. +2. Update the relevant component plan when the gap is local. +3. Update [`decisions.md`](./decisions.md) when the gap is "we just + haven't decided yet, but we need to." +4. Update [`deferred.md`](./deferred.md) when the gap is "this is + real work but not for alpha." + +Don't add a new top-level numbered doc. diff --git a/benchmarks-website/planning/benchmark-mapping.md b/benchmarks-website/planning/benchmark-mapping.md new file mode 100644 index 00000000000..9216a45ebc4 --- /dev/null +++ b/benchmarks-website/planning/benchmark-mapping.md @@ -0,0 +1,147 @@ + + +# Existing benchmarks → fact-table mapping + +A cross-reference from today's benchmark code to the v3 fact tables +in [`01-schema.md`](./01-schema.md). Use this when implementing +emitter `to_v3_json` (component plan in +[`components/emitter.md`](./components/emitter.md)) or when sanity- +checking that the schema is expressive enough. + +If a benchmark in this repo is not listed here, it is either +deferred to phase 2 or out of scope for the bench website. + +## Source measurement type → target table + +The canonical mapping. The Rust types live in +`vortex-bench/src/measurements.rs` (and per-benchmark crates). + +| Source type | Wire `kind` | Target table | Notes | +|---|---|---|---| +| `QueryMeasurement` (paired with `MemoryMeasurement`) | `query_measurement` | `query_measurements` | The two structs collapse into **one** v3 record. Memory fields are omitted if `--track-memory` was off. | +| `TimingMeasurement` (only the random-access variant uses this today) | `random_access_time` | `random_access_times` | | +| `CompressionTimingMeasurement` | `compression_time` (with `op ∈ {encode, decode}`) | `compression_times` | The `op` is decided by which side of `compress-bench`'s timing loop produced it. | +| `CustomUnitMeasurement` with byte unit (sizes) | `compression_size` | `compression_sizes` | A new `CompressionSizeMeasurement` extraction lives in `vortex-bench/src/compress/mod.rs`; the emitter no longer rides on `CustomUnitMeasurement`. | +| `CustomUnitMeasurement` with `ratio` unit | **dropped** | none | Computed at read time from `compression_sizes`. | +| `ScanTiming` (vector-search) | `vector_search_run` | `vector_search_runs` | Carries timing **plus** the three counters in the same row. | + +## Per-binary inventory + +Every benchmark binary in this repo, the measurement structs it +produces today, and the v3 tables those measurements land in. + +### `benchmarks/datafusion-bench` + +Runs the SQL query suites with `engine = datafusion`, parameterized +over a `Format` (parquet, vortex-file-compressed, vortex-compact, +arrow, lance via the lance-bench wrapper). + +- Produces `QueryMeasurement` (+ `MemoryMeasurement` when + `--track-memory`) → **`query_measurements`**. +- One row per `(commit, dataset, dataset_variant, scale_factor, + query_idx, storage, engine = "datafusion", format)`. + +### `benchmarks/duckdb-bench` + +Same as `datafusion-bench` but with `engine = duckdb`. + +- Produces `QueryMeasurement` (+ `MemoryMeasurement` when tracking) + → **`query_measurements`**, with `engine = "duckdb"`. + +### `benchmarks/lance-bench` + +Three things in one crate: + +1. **Query runner** (`src/main.rs`): `engine = datafusion`, + `format = lance` only. Produces `QueryMeasurement` (+ + `MemoryMeasurement`) → **`query_measurements`**. +2. **Compression runner** (`src/compress.rs`): produces + `CompressionTimingMeasurement` + size `CustomUnitMeasurement` → + **`compression_times`** (with `op ∈ {encode, decode}`, + `format = lance`) and **`compression_sizes`** + (`format = lance`). +3. **Random-access runner** (`src/random_access.rs`): produces + `TimingMeasurement` → **`random_access_times`** with + `format = lance`. + +### `benchmarks/compress-bench` + +The compression suite. Per dataset, runs encode + decode against +each enabled `Format` and records the resulting on-disk size. + +- `CompressionTimingMeasurement` for encode → **`compression_times`** + with `op = "encode"`. +- `CompressionTimingMeasurement` for decode → **`compression_times`** + with `op = "decode"`. +- Byte-unit `CustomUnitMeasurement` (the size entries) → + **`compression_sizes`**. +- Ratio-unit `CustomUnitMeasurement` (the `vortex:parquet-zstd + ratio/...` entries) → **dropped**. The reader recomputes ratios + from `compression_sizes`. + +### `benchmarks/random-access-bench` + +The random-access "take" timing suite. Datasets here (chimp, taxi, +etc.) are a different namespace from the SQL query suites. + +- `TimingMeasurement` → **`random_access_times`**. +- `format` is one of `vortex-file-compressed`, `vortex-compact`, + `parquet`, `lance`. + +### `benchmarks/vector-search-bench` + +Cosine-similarity scan over a vector dataset. Each dataset/layout/ +flavor combination produces a single `ScanTiming` per scan +configuration. + +- `ScanTiming` → **`vector_search_runs`**. +- `dataset` from `VectorDataset` (e.g. `cohere-large-10m`). +- `layout` from `TrainLayout`. +- `flavor` from `VectorFlavor` (compression flavor; the vector- + search analogue of `format`). +- `threshold`, `iterations` are real columns. +- `query_seed` is **not** stored - it's a deterministic seed for + the query sampler and not a measurement dimension. + +## Per-suite dim values + +For SQL query suites (everything that flows through +`query_measurements`), the dim columns are populated as follows: + +| `BenchmarkArg` | `dataset` | `dataset_variant` | `scale_factor` | Notes | +|---|---|---|---|---| +| `TpcH` | `tpch` | NULL | TPC SF as string (`"1"`, `"10"`, `"100"`, `"1000"`) | | +| `TpcDS` | `tpcds` | NULL | TPC SF as string | | +| `ClickBench` | `clickbench` | flavor as string (`partitioned` / `single`) | NULL | The flavor lives in `dataset_variant`, not `dataset`. | +| `StatPopGen` | `statpopgen` | NULL | n_rows as string | `scale_factor` here is the row count; the per-dataset interpretation of SF is documented in [`01-schema.md`](./01-schema.md). | +| `PolarSignals` | `polarsignals` | NULL | n_rows as string | Same SF interpretation as StatPopGen. | +| `Fineweb` | `fineweb` | NULL | NULL | | +| `GhArchive` | `gharchive` | NULL | NULL | | +| `PublicBi` | `public-bi` | dataset name (e.g. `cms-provider`) | NULL | The Public-BI sub-dataset name lives in `dataset_variant`. | + +For non-query suites: + +- `compress-bench`: `dataset` is the compression dataset name; if + the suite later grows variants, `dataset_variant` is available. +- `random-access-bench`: `dataset` is the random-access dataset + name. No variant column on this table. +- `vector-search-bench`: see the [vector_search_runs + table](./01-schema.md#vector_search_runs). + +## What this implies for the emitter + +The mapping above is the contract `vortex-bench --gh-json-v3` +implements. Any v3 record an emitter writes today must land in +exactly one of the five tables; if a future measurement type +doesn't fit, that's the signal to add a sixth table (and a sixth +`kind`) rather than overload one of these. + +The **historical migrator** will use the same mapping when it lands +(it's deferred - see [`deferred.md`](./deferred.md#historical-data-migration)). +The v2 classifier on `develop` at `benchmarks-website/server.js` +becomes useful then, because the v2 S3 dump pre-dates the +discriminator and we'll have to recover `kind` from name strings. +For new ingest at alpha, no classifier is needed. diff --git a/benchmarks-website/planning/components/emitter.md b/benchmarks-website/planning/components/emitter.md new file mode 100644 index 00000000000..e462a9804c8 --- /dev/null +++ b/benchmarks-website/planning/components/emitter.md @@ -0,0 +1,86 @@ + + +# Component: Emitter (alpha) + +## Required reading + +- [`../00-overview.md`](../00-overview.md) +- [`../02-contracts.md`](../02-contracts.md) +- [`../benchmark-mapping.md`](../benchmark-mapping.md) - the + source-type → target-table mapping. + +## Goal + +Extend `vortex-bench` so it emits v3-shape JSON. Plus a small POST +script that wraps the JSONL in an envelope and sends it to a +running alpha server. + +This is **purely additive** to v2's emission path. Nothing in v2 is +touched. CI workflow integration, dual-write, the orchestrator +update, and the outbox safety net all wait until after the alpha +loop works end-to-end (see [`../deferred.md`](../deferred.md)). + +## In scope + +### Rust emitter + +- Add a `--gh-json-v3 ` CLI flag that writes JSONL of bare + v3 records (no envelope). The legacy `-d gh-json -o ...` form is + untouched - both work at alpha. +- Emit a record with the appropriate `kind` for every measurement + type produced today. The mapping from existing measurement + structs to wire `kind`s is the table in + [`../benchmark-mapping.md`](../benchmark-mapping.md). +- Two non-obvious points (everything else is mechanical): + - `QueryMeasurement` and the paired `MemoryMeasurement` collapse + into **one** `query_measurement` record with both `value_ns` + and the four memory fields. If memory wasn't tracked, omit the + memory fields. + - Vector-search's `ScanTiming` doesn't carry its own dataset / + layout / threshold (those live in the binary's `Args`). The + emitter has to plumb them through to the record. +- `CustomUnitMeasurement` cross-format ratios are **not emitted** - + ratios are computed in the read path. +- Snapshot tests per `kind` (any framework), scrubbing `commit_sha` + and `env_triple`. + +### Post-ingest script + +A small Python script (path of the agent's choosing, e.g. under +`scripts/`) that: + +- Reads JSONL of records. +- Fills the `commit` envelope fields by shelling out to `git show` + (or equivalent) for the SHA passed as an argument. +- Wraps the records in the envelope from + [`../02-contracts.md`](../02-contracts.md). +- POSTs to `/api/ingest` with the bearer token. +- Exits non-zero on 4xx / 5xx. **No retries, no spool, no S3 + outbox at alpha** - those land when CI starts using this. + +## Out of scope (deferred) + +- Replacing the v2 `-d`/`-o` CLI form. Both forms coexist at alpha. +- Removing the v2 `gh-json` emission path. +- Updating `bench-orchestrator` or any GitHub Actions workflows. + Alpha runs are manual. +- Retry / spool / outbox-drain on POST failures. + +See [`../deferred.md`](../deferred.md) for the post-alpha plan. + +## Acceptance criteria + +- `cargo test -p vortex-bench` passes; one snapshot per `kind`. +- Running a benchmark with `--gh-json-v3 ` writes valid JSONL + matching the wire shape from + [`../02-contracts.md`](../02-contracts.md). +- The post-ingest script round-trips a fixture file through a + running alpha server (200 with non-zero `inserted` on first run, + 200 with non-zero `updated` on second run). + +## Branch + +`claude/benchmarks-v3-emitter` diff --git a/benchmarks-website/planning/components/server.md b/benchmarks-website/planning/components/server.md new file mode 100644 index 00000000000..301bb7e4bf9 --- /dev/null +++ b/benchmarks-website/planning/components/server.md @@ -0,0 +1,70 @@ + + +# Component: Server (alpha) + +## Required reading + +- [`../00-overview.md`](../00-overview.md) +- [`../01-schema.md`](../01-schema.md) +- [`../02-contracts.md`](../02-contracts.md) + +## Goal + +A single Rust binary: an HTTP server that owns a DuckDB file on +local disk, accepts authenticated `/api/ingest` POSTs, and serves +enough of a read API to render one chart page. + +This is the **alpha** version. It runs locally or on a dev box; no +production deploy. Production deploy, backups, admin tooling, and +historical data import are deferred (see +[`../deferred.md`](../deferred.md)). + +The server crate lives at a path of the agent's choosing under +`benchmarks-website/`, registered as a workspace member. + +## In scope + +- Open the DuckDB file and apply the schema DDL on boot. No + migration framework yet - if the schema changes during alpha, + delete the file and re-run. +- Bearer-token middleware on `/api/ingest`. Token from + `INGEST_BEARER_TOKEN` env var, constant-time compared. +- `POST /api/ingest`: parse the envelope from + [`../02-contracts.md`](../02-contracts.md), upsert the commit, + dispatch each record to its destination fact table by `kind`, + enforce all-or-nothing per POST. Compute each row's + `measurement_id` server-side as part of the INSERT. Return + `{ inserted, updated }` aggregated across tables. +- `GET /api/groups` and `GET /api/chart/:slug`: enough to render + one chart page. Slugs round-trip; the agent picks the format. +- `GET /health`: enough to confirm the DB is open and ingest is + working (path, latest commit timestamp, per-table row counts - + exact shape is the agent's call). +- Mount whatever HTML routes the web-ui component contributes. + +Framework, templating engine (`maud` or `askama`), DuckDB driver +version, module layout, and DB-access concurrency model are the +agent's call. Pin the DuckDB crate version in `Cargo.toml`. + +## Out of scope (deferred) + +Schema migrations, lookup tables, pre-built views, multi-page read +API, admin endpoints, containerization, EBS mount, backups. See +[`../deferred.md`](../deferred.md). + +## Acceptance criteria + +- `cargo build` succeeds for the server crate. +- Integration test: POST a fixture envelope with a valid bearer → + 200; POST again → 200 with `updated > 0, inserted = 0`; POST + with no/wrong bearer → 401; POST with an unknown `kind` → 400. +- `GET /health` returns a coherent shape after an ingest. +- `cargo run` for the server, pointed at a fresh DuckDB file, + serves both read routes locally. + +## Branch + +`claude/benchmarks-v3-server` diff --git a/benchmarks-website/planning/components/web-ui.md b/benchmarks-website/planning/components/web-ui.md new file mode 100644 index 00000000000..abf9e9de4a0 --- /dev/null +++ b/benchmarks-website/planning/components/web-ui.md @@ -0,0 +1,62 @@ + + +# Component: Web UI (alpha) + +## Required reading + +- [`../00-overview.md`](../00-overview.md) +- [`../01-schema.md`](../01-schema.md) +- [`../02-contracts.md`](../02-contracts.md) - the JSON shapes you + render against. + +## Goal + +Get something on screen. **One landing page** that lists groups and +**one chart page** that renders a single chart. SSR HTML + a thin +Chart.js hydration. That's it for alpha. + +This component develops in parallel against a fixture-populated +DuckDB - no dependency on the live ingest path. + +## In scope + +- A fixture: a small DuckDB file (or a builder that produces one + from a JSONL fixture) covering all five fact tables with a + handful of records each. Used for dev and tests. +- Landing page (`GET /`): list of groups with links into chart + pages, derived from `/api/groups`. +- Chart page (`GET /chart/:slug`): one Chart.js line chart, data + embedded inline as a JSON ` +tpch sf=1 Q1 [nvme] — bench.vortex.dev

unit: ns · 2 series · 3 commits

From fd4ee6aa96db18c7ab2b750020136616e52886ed Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sun, 26 Apr 2026 14:54:24 -0400 Subject: [PATCH 08/12] add duckdb to gitignore Signed-off-by: Connor Tsui --- .gitignore | 3 +++ REUSE.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7fa79fb2162..6a996cf96cc 100644 --- a/.gitignore +++ b/.gitignore @@ -242,3 +242,6 @@ trace*.pb # pytest-benchmark output vortex-python/.benchmarks/ +# For local benchmarks website server and things like the WAL +**.duckdb* + diff --git a/REUSE.toml b/REUSE.toml index 161f6e3086a..8e406c95c90 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -36,7 +36,7 @@ SPDX-FileCopyrightText = "Copyright the Vortex contributors" SPDX-License-Identifier = "CC-BY-4.0" [[annotations]] -path = ["**/.gitignore", ".gitmodules", ".python-version", "**/*.lock", "**/*.lockfile", "**/*.toml", "**/*.json", ".idea/**", ".github/**", "codecov.yml", "java/gradle/wrapper/gradle-wrapper.properties"] +path = ["**/.gitignore", ".gitmodules", ".python-version", "**/*.lock", "**/*.lockfile", "**/*.toml", "**/*.json", ".idea/**", ".github/**", "codecov.yml", "java/gradle/wrapper/gradle-wrapper.properties", "**.duckdb*"] precedence = "override" SPDX-FileCopyrightText = "Copyright the Vortex contributors" SPDX-License-Identifier = "Apache-2.0" From 814c609b2528fa4eb27da21289633644ece9d5c7 Mon Sep 17 00:00:00 2001 From: Connor Tsui <87130162+connortsui20@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:12:29 -0400 Subject: [PATCH 09/12] [claude] Add vortex-bench-server v3 deployment infrastructure (#7644) This PR introduces the deployment infrastructure for vortex-bench-server v3, a new benchmarking server that runs alongside the existing v2 instance. The v3 server provides an ingest endpoint for benchmark results with bearer token authentication and uses DuckDB for data storage. 1. **GitHub Actions workflow** (`publish-bench-server.yml`): New CI pipeline that builds and publishes the vortex-bench-server Docker image to GHCR on changes to the server code, vortex-bench crate, or Cargo.lock. 2. **Dockerfile** (`benchmarks-website/server/Dockerfile`): Multi-stage Docker build that: - Compiles vortex-bench-server in a Rust 1.91 environment - Packages it with DuckDB CLI tools in a minimal Debian image - Targets ARM64 architecture for EC2 deployment 3. **Backup script** (`benchmarks-website/server/scripts/backup.sh`): Daily backup utility that: - Exports the DuckDB database from the running container - Uploads backups to S3 (`vortex-ci-benchmark-results/v3-backups/`) - Manages local disk space by retaining only the latest backup 4. **Docker Compose configuration**: Added vortex-bench-server service that: - Runs on port 3001 (v2 remains on port 80) - Mounts EBS-backed data directory for DuckDB persistence - Loads bearer token from `/etc/vortex-bench/secrets.env` - Integrates with existing watchtower for automatic image updates 5. **EC2 initialization guide** (`ec2-init.txt`): Comprehensive setup documentation covering: - Bearer token secret management - EBS volume preparation - Service startup and health checks - Cron-based backup scheduling - Token rotation procedures The v3 server is designed to run additively alongside v2, allowing for gradual DNS migration and dual-write support from CI. The Docker image build is validated by the GitHub Actions workflow on each push to develop. The backup script can be tested manually on the EC2 host before cron scheduling. Smoke tests are documented in the setup guide (curl against `/health` endpoint on port 3001). https://claude.ai/code/session_019mBcBdF4LhKDXyKwuKRAPV --------- Signed-off-by: Claude Co-authored-by: Claude --- .github/workflows/publish-bench-server.yml | 46 ++ Cargo.lock | 574 +++++++------------- benchmarks-website/docker-compose.yml | 14 + benchmarks-website/ec2-init.txt | 55 +- benchmarks-website/server/Cargo.toml | 3 +- benchmarks-website/server/Dockerfile | 46 ++ benchmarks-website/server/scripts/backup.sh | 46 ++ 7 files changed, 414 insertions(+), 370 deletions(-) create mode 100644 .github/workflows/publish-bench-server.yml create mode 100644 benchmarks-website/server/Dockerfile create mode 100755 benchmarks-website/server/scripts/backup.sh diff --git a/.github/workflows/publish-bench-server.yml b/.github/workflows/publish-bench-server.yml new file mode 100644 index 00000000000..0bfcb6d3293 --- /dev/null +++ b/.github/workflows/publish-bench-server.yml @@ -0,0 +1,46 @@ +name: Publish Bench Server + +on: + push: + branches: [develop] + paths: + - "benchmarks-website/server/**" + - "vortex-bench/**" + - "Cargo.lock" + - ".github/workflows/publish-bench-server.yml" + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + id-token: write + steps: + - uses: actions/checkout@v6 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: . + file: ./benchmarks-website/server/Dockerfile + platforms: linux/arm64 + push: true + tags: | + ghcr.io/${{ github.repository }}/vortex-bench-server:latest + ghcr.io/${{ github.repository }}/vortex-bench-server:${{ github.sha }} diff --git a/Cargo.lock b/Cargo.lock index e0016f813c2..d8b13b8ae20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arc-swap" @@ -225,24 +228,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "arrow" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e833808ff2d94ed40d9379848a950d995043c7fb3e81a30b383f4c6033821cc" -dependencies = [ - "arrow-arith 56.2.0", - "arrow-array 56.2.0", - "arrow-buffer 56.2.0", - "arrow-cast 56.2.0", - "arrow-data 56.2.0", - "arrow-ord 56.2.0", - "arrow-row 56.2.0", - "arrow-schema 56.2.0", - "arrow-select 56.2.0", - "arrow-string 56.2.0", -] - [[package]] name = "arrow" version = "57.3.0" @@ -285,20 +270,6 @@ dependencies = [ "arrow-string 58.1.0", ] -[[package]] -name = "arrow-arith" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad08897b81588f60ba983e3ca39bda2b179bdd84dced378e7df81a5313802ef8" -dependencies = [ - "arrow-array 56.2.0", - "arrow-buffer 56.2.0", - "arrow-data 56.2.0", - "arrow-schema 56.2.0", - "chrono", - "num", -] - [[package]] name = "arrow-arith" version = "57.3.0" @@ -327,22 +298,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-array" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8548ca7c070d8db9ce7aa43f37393e4bfcf3f2d3681df278490772fd1673d08d" -dependencies = [ - "ahash 0.8.12", - "arrow-buffer 56.2.0", - "arrow-data 56.2.0", - "arrow-schema 56.2.0", - "chrono", - "half", - "hashbrown 0.16.1", - "num", -] - [[package]] name = "arrow-array" version = "57.3.0" @@ -381,17 +336,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-buffer" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e003216336f70446457e280807a73899dd822feaf02087d31febca1363e2fccc" -dependencies = [ - "bytes", - "half", - "num", -] - [[package]] name = "arrow-buffer" version = "57.3.0" @@ -416,27 +360,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-cast" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919418a0681298d3a77d1a315f625916cb5678ad0d74b9c60108eb15fd083023" -dependencies = [ - "arrow-array 56.2.0", - "arrow-buffer 56.2.0", - "arrow-data 56.2.0", - "arrow-schema 56.2.0", - "arrow-select 56.2.0", - "atoi", - "base64", - "chrono", - "comfy-table", - "half", - "lexical-core", - "num", - "ryu", -] - [[package]] name = "arrow-cast" version = "57.3.0" @@ -511,18 +434,6 @@ dependencies = [ "regex", ] -[[package]] -name = "arrow-data" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5c64fff1d142f833d78897a772f2e5b55b36cb3e6320376f0961ab0db7bd6d0" -dependencies = [ - "arrow-buffer 56.2.0", - "arrow-schema 56.2.0", - "half", - "num", -] - [[package]] name = "arrow-data" version = "57.3.0" @@ -629,19 +540,6 @@ dependencies = [ "simdutf8", ] -[[package]] -name = "arrow-ord" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8f82583eb4f8d84d4ee55fd1cb306720cddead7596edce95b50ee418edf66f" -dependencies = [ - "arrow-array 56.2.0", - "arrow-buffer 56.2.0", - "arrow-data 56.2.0", - "arrow-schema 56.2.0", - "arrow-select 56.2.0", -] - [[package]] name = "arrow-ord" version = "57.3.0" @@ -668,19 +566,6 @@ dependencies = [ "arrow-select 58.1.0", ] -[[package]] -name = "arrow-row" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d07ba24522229d9085031df6b94605e0f4b26e099fb7cdeec37abd941a73753" -dependencies = [ - "arrow-array 56.2.0", - "arrow-buffer 56.2.0", - "arrow-data 56.2.0", - "arrow-schema 56.2.0", - "half", -] - [[package]] name = "arrow-row" version = "57.3.0" @@ -707,15 +592,6 @@ dependencies = [ "half", ] -[[package]] -name = "arrow-schema" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3aa9e59c611ebc291c28582077ef25c97f1975383f1479b12f3b9ffee2ffabe" -dependencies = [ - "bitflags", -] - [[package]] name = "arrow-schema" version = "57.3.0" @@ -738,20 +614,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "arrow-select" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c41dbbd1e97bfcaee4fcb30e29105fb2c75e4d82ae4de70b792a5d3f66b2e7a" -dependencies = [ - "ahash 0.8.12", - "arrow-array 56.2.0", - "arrow-buffer 56.2.0", - "arrow-data 56.2.0", - "arrow-schema 56.2.0", - "num", -] - [[package]] name = "arrow-select" version = "57.3.0" @@ -780,23 +642,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-string" -version = "56.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f5183c150fbc619eede22b861ea7c0eebed8eaac0333eaa7f6da5205fd504d" -dependencies = [ - "arrow-array 56.2.0", - "arrow-buffer 56.2.0", - "arrow-data 56.2.0", - "arrow-schema 56.2.0", - "arrow-select 56.2.0", - "memchr", - "num", - "regex", - "regex-syntax", -] - [[package]] name = "arrow-string" version = "57.3.0" @@ -845,9 +690,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -1235,9 +1080,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -1508,9 +1353,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -1524,12 +1369,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cexpr" version = "0.6.0" @@ -1816,12 +1655,12 @@ dependencies = [ [[package]] name = "comfy-table" -version = "7.1.2" +version = "7.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" dependencies = [ - "strum 0.26.3", - "strum_macros 0.26.4", + "crossterm 0.28.1", + "unicode-segmentation", "unicode-width 0.2.2", ] @@ -1863,9 +1702,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "bzip2", "compression-core", @@ -1878,9 +1717,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "concurrent-queue" @@ -2121,6 +1960,19 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + [[package]] name = "crossterm" version = "0.29.0" @@ -3761,6 +3613,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -3825,7 +3688,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3873,12 +3736,13 @@ checksum = "ab23e69df104e2fd85ee63a533a22d2132ef5975dc6b36f9f3e5a7305e4a8ed7" [[package]] name = "duckdb" -version = "1.4.1" +version = "1.10502.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a093eed1c714143b257b95fa323e38527fabf05fbf02bb0d5d2045275ffdaef" +checksum = "0fdc796383b176dd5a45353fbb5e64583c0ee4da12cb62c9e510b785324b2488" dependencies = [ - "arrow 56.2.0", + "arrow 58.1.0", "cast", + "comfy-table", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4024,7 +3888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4243,9 +4107,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2195cc7f87e84bd695586137de99605e7e9579b26ec5e01b82960ddb4d0922f2" +checksum = "2b3a6f3550e61b999febd7168d462db953948eff4fc3448276b3d10d10324dbb" dependencies = [ "arrow-array 57.3.0", "rand 0.9.4", @@ -4700,9 +4564,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] @@ -4743,6 +4607,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -5046,7 +4911,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5108,9 +4973,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -5118,14 +4983,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -5147,22 +5012,6 @@ dependencies = [ "jiff-tzdb", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - [[package]] name = "jni" version = "0.22.4" @@ -5173,7 +5022,7 @@ dependencies = [ "combine", "java-locator", "jni-macros", - "jni-sys 0.4.1", + "jni-sys", "libloading", "log", "simd_cesu8", @@ -5195,15 +5044,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - [[package]] name = "jni-sys" version = "0.4.1" @@ -5303,9 +5143,9 @@ checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" [[package]] name = "lance" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe6c3ddd79cdfd2b7e1c23cafae52806906bc40fbd97de9e8cf2f8c7a75fc04" +checksum = "f63e285ceee2b4ca8eb3a8742266cc1ac8161599767a8ecb4d8c2f9fd43d8b29" dependencies = [ "arrow 57.3.0", "arrow-arith 57.3.0", @@ -5369,9 +5209,9 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d9f5d95bdda2a2b790f1fb8028b5b6dcf661abeb3133a8bca0f3d24b054af87" +checksum = "5c55e62fc04422ef4cd4af6f863ada32641ae23124f9b2e9c567a40d617e8c97" dependencies = [ "arrow-array 57.3.0", "arrow-buffer 57.3.0", @@ -5409,9 +5249,9 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f827d6ab9f8f337a9509d5ad66a12f3314db8713868260521c344ef6135eb4e4" +checksum = "a48d232a2908645af0040f96c60a6387fea2df75e762d7033e93e17bb420c6a1" dependencies = [ "arrayref", "paste", @@ -5420,9 +5260,9 @@ dependencies = [ [[package]] name = "lance-core" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e25df6a79bf72ee6bcde0851f19b1cd36c5848c1b7db83340882d3c9fdecb" +checksum = "ce071baaff88fcdcf67f1dd0af54e17656f52ae75aaeb75f25f9cf4da29241f2" dependencies = [ "arrow-array 57.3.0", "arrow-buffer 57.3.0", @@ -5459,9 +5299,9 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93146de8ae720cb90edef81c2f2d0a1b065fc2f23ecff2419546f389b0fa70a4" +checksum = "11ebc97ee94fa8e1af6fd0520066c7e7e0eab38a100e750ba9aabad644c5aa57" dependencies = [ "arrow 57.3.0", "arrow-array 57.3.0", @@ -5491,9 +5331,9 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccec8ce4d8e0a87a99c431dab2364398029f2ffb649c1a693c60c79e05ed30dd" +checksum = "9b90dbb2829875b3a3d00f88fd3a3e39a9e4c7d34c266f67da6550fcda54c76e" dependencies = [ "arrow 57.3.0", "arrow-array 57.3.0", @@ -5511,9 +5351,9 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1aec0bbbac6bce829bc10f1ba066258126100596c375fb71908ecf11c2c2a5" +checksum = "65ec429cc2e18ad1b7e43cc7ec57a2f2e49229cfbd934da45e619751a886b8cd" dependencies = [ "arrow-arith 57.3.0", "arrow-array 57.3.0", @@ -5550,9 +5390,9 @@ dependencies = [ [[package]] name = "lance-file" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a8c548804f5b17486dc2d3282356ed1957095a852780283bc401fdd69e9075" +checksum = "418afe3f82487615fa09222b95a4b5853103f3f0425996d24a537ca750381f83" dependencies = [ "arrow-arith 57.3.0", "arrow-array 57.3.0", @@ -5584,9 +5424,9 @@ dependencies = [ [[package]] name = "lance-index" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da212f0090ea59f79ac3686660f596520c167fe1cb5f408900cf71d215f0e03" +checksum = "936b3deeb6ee075646d18f27b01cf2d2e846c3f5f6c5fa45b30aa41dd5b4c4e2" dependencies = [ "arrow 57.3.0", "arrow-arith 57.3.0", @@ -5650,9 +5490,9 @@ dependencies = [ [[package]] name = "lance-io" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d958eb4b56f03bbe0f5f85eb2b4e9657882812297b6f711f201ffc995f259f" +checksum = "4103e4cebe146af15bfb198c8142d6ea37d5b25fa04158bf2d9be4597bf174d3" dependencies = [ "arrow 57.3.0", "arrow-arith 57.3.0", @@ -5689,9 +5529,9 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0285b70da35def7ed95e150fae1d5308089554e1290470403ed3c50cb235bc5e" +checksum = "c00c7ad71eca93635404519e77add6689947c9342134bb2133578f81249bf809" dependencies = [ "arrow-array 57.3.0", "arrow-buffer 57.3.0", @@ -5707,9 +5547,9 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f78e2a828b654e062a495462c6e3eb4fcf0e7e907d761b8f217fc09ccd3ceac" +checksum = "e0c59a574e72a4b72da8096bcaaa1b1e5b44f6a83da164cc714c286fab30c369" dependencies = [ "arrow 57.3.0", "async-trait", @@ -5735,9 +5575,9 @@ dependencies = [ [[package]] name = "lance-table" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df9c4adca3eb2074b3850432a9fb34248a3d90c3d6427d158b13ff9355664ee" +checksum = "943b9c503f23ebab9e0dbee356f528bc4cbcafded87a6848451f205b0bb473d7" dependencies = [ "arrow 57.3.0", "arrow-array 57.3.0", @@ -5890,23 +5730,25 @@ checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libduckdb-sys" -version = "1.4.1" +version = "1.10502.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b93c3ff279601516f01531cadf2ccba50394fbb5f7bf685c6e6b9b07c8dca6f" +checksum = "8d7401630ae2abcff642f7156294289e50f2d222e061c026ad797b01bf20c215" dependencies = [ "cc", "flate2", "pkg-config", + "reqwest 0.12.28", "serde", "serde_json", "tar", "vcpkg", + "zip 6.0.0", ] [[package]] @@ -6509,21 +6351,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", + "windows-sys 0.61.2", ] [[package]] @@ -6561,28 +6389,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -7209,9 +7015,9 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "044b1fa4f259f4df9ad5078e587b208f5d288a25407575fcddb9face30c7c692" dependencies = [ - "rand 0.8.6", + "rand 0.9.4", "socket2", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -7708,7 +7514,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7934,7 +7740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" dependencies = [ "cfg-if", - "crossterm", + "crossterm 0.29.0", "instability", "ratatui-core", ] @@ -8173,13 +7979,14 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams 0.4.2", "web-sys", + "webpki-roots", ] [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", @@ -8264,9 +8071,9 @@ dependencies = [ [[package]] name = "roaring" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ba9ce64a8f45d7fc86358410bb1a82e8c987504c0d4900e9141d69a9f26c885" +checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9" dependencies = [ "bytemuck", "byteorder", @@ -8393,14 +8200,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ "aws-lc-rs", "once_cell", @@ -8425,9 +8232,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -8435,13 +8242,13 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls", @@ -8451,7 +8258,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9552,7 +9359,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9571,7 +9378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9590,7 +9397,7 @@ dependencies = [ "chrono", "num_cpus", "ping", - "reqwest 0.13.2", + "reqwest 0.13.3", "sysinfo", "test-with-derive", "uzers", @@ -9611,7 +9418,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "syn 2.0.117", "sysinfo", "uzers", @@ -9941,9 +9748,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -10104,11 +9911,11 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "twox-hash" -version = "2.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" dependencies = [ - "rand 0.8.6", + "rand 0.9.4", ] [[package]] @@ -10517,7 +10324,7 @@ dependencies = [ "parquet 58.1.0", "rand 0.10.1", "regex", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "sysinfo", @@ -10547,7 +10354,7 @@ dependencies = [ "duckdb", "insta", "maud", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "subtle", @@ -10637,7 +10444,7 @@ dependencies = [ "clap", "futures", "parquet 58.1.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "sha2 0.11.0", @@ -10819,7 +10626,7 @@ dependencies = [ "object_store 0.13.2", "parking_lot", "paste", - "reqwest 0.13.2", + "reqwest 0.13.3", "rstest", "tempfile", "tracing", @@ -10829,7 +10636,7 @@ dependencies = [ "vortex-runend", "vortex-sequence", "vortex-utils", - "zip", + "zip 8.6.0", ] [[package]] @@ -11041,7 +10848,7 @@ dependencies = [ "arrow-array 58.1.0", "arrow-schema 58.1.0", "futures", - "jni 0.22.4", + "jni", "object_store 0.13.2", "parking_lot", "thiserror 2.0.18", @@ -11121,7 +10928,7 @@ dependencies = [ "bindgen", "libloading", "liblzma", - "reqwest 0.13.2", + "reqwest 0.13.3", "tar", "vortex-cuda-macros", ] @@ -11332,7 +11139,7 @@ dependencies = [ "arrow-schema 58.1.0", "clap", "console_error_panic_hook", - "crossterm", + "crossterm 0.29.0", "datafusion 53.1.0", "env_logger", "flatbuffers", @@ -11601,6 +11408,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "8.0.2" @@ -11632,7 +11448,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11753,15 +11569,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -11782,26 +11589,20 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.61.2" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-link", + "windows-targets 0.53.5", ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-link", ] [[package]] @@ -11813,7 +11614,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", @@ -11821,19 +11622,30 @@ dependencies = [ ] [[package]] -name = "windows-threading" -version = "0.2.1" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "windows-threading" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] [[package]] name = "windows_aarch64_gnullvm" @@ -11842,10 +11654,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -11854,10 +11666,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "windows_aarch64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -11865,6 +11677,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" @@ -11872,10 +11690,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -11884,10 +11702,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -11896,10 +11714,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -11908,10 +11726,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -11919,6 +11737,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.15" @@ -12204,6 +12028,20 @@ dependencies = [ "num-traits", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + [[package]] name = "zip" version = "8.6.0" diff --git a/benchmarks-website/docker-compose.yml b/benchmarks-website/docker-compose.yml index 4c2e9682329..b97482a230a 100644 --- a/benchmarks-website/docker-compose.yml +++ b/benchmarks-website/docker-compose.yml @@ -5,6 +5,20 @@ services: - "80:3000" restart: unless-stopped + vortex-bench-server: + image: ghcr.io/vortex-data/vortex/vortex-bench-server:latest + ports: + - "3001:3000" + environment: + VORTEX_BENCH_DB: "/app/data/bench.duckdb" + VORTEX_BENCH_BIND: "0.0.0.0:3000" + VORTEX_BENCH_LOG: "info,vortex_bench_server=debug" + env_file: + - /etc/vortex-bench/secrets.env + volumes: + - /opt/benchmarks-website/data:/app/data + restart: unless-stopped + watchtower: image: containrrr/watchtower volumes: diff --git a/benchmarks-website/ec2-init.txt b/benchmarks-website/ec2-init.txt index 1c2459b3bee..4e1377cc014 100644 --- a/benchmarks-website/ec2-init.txt +++ b/benchmarks-website/ec2-init.txt @@ -14,4 +14,57 @@ sudo mkdir -p /opt/benchmarks-website sudo cp docker-compose.yml /opt/benchmarks-website/ cd /opt/benchmarks-website - docker compose up -d \ No newline at end of file + docker compose up -d + + ==================================================================== + v3 (vortex-bench-server) — additive setup, runs alongside v2 + ==================================================================== + + v2 stays on port 80 until DNS is flipped. v3 runs on port 3001 from + the same docker-compose.yml on this host. + + 4. Create the bearer-token env file (root:root, mode 600) + sudo mkdir -p /etc/vortex-bench + sudo install -m 600 -o root -g root /dev/null /etc/vortex-bench/secrets.env + # Edit and set INGEST_BEARER_TOKEN=: + sudo vi /etc/vortex-bench/secrets.env + # File contents: + # INGEST_BEARER_TOKEN= + + 5. Create the EBS-backed DuckDB data directory + # Assumes an EBS volume is already mounted at /opt/benchmarks-website/data. + sudo mkdir -p /opt/benchmarks-website/data + sudo chown root:root /opt/benchmarks-website/data + sudo chmod 755 /opt/benchmarks-website/data + + 6. Pull and start v3 (watchtower already polls ghcr.io for refreshes) + cd /opt/benchmarks-website + docker compose pull vortex-bench-server + docker compose up -d vortex-bench-server + # Smoke-check on the host: + curl -sf http://127.0.0.1:3001/health || echo "v3 not responding" + + 7. Install the daily DuckDB backup cron + # Copy the backup script from the repo checkout to a stable location. + sudo install -m 755 -o root -g root \ + benchmarks-website/server/scripts/backup.sh \ + /usr/local/bin/vortex-bench-backup.sh + # Cron entry: 06:00 UTC daily, after the nightly bench finishes. + sudo tee /etc/cron.d/vortex-bench-backup >/dev/null <<'CRON' + 0 6 * * * root /usr/local/bin/vortex-bench-backup.sh >> /var/log/vortex-bench-backup.log 2>&1 + CRON + sudo chmod 644 /etc/cron.d/vortex-bench-backup + # The instance IAM role already permits writes to + # s3://vortex-ci-benchmark-results/ (same role v2's cat-s3.sh uses). + + 8. Bearer-token rotation procedure + # When rotating INGEST_BEARER_TOKEN: + # a. Generate a new token (e.g. `openssl rand -hex 32`). + # b. Update the GitHub Actions Environment secret INGEST_BEARER_TOKEN + # so CI dual-writes use the new value. + # c. On this EC2 host, edit the env file and restart only the v3 + # container so v2 traffic on port 80 is unaffected: + # sudo vi /etc/vortex-bench/secrets.env + # cd /opt/benchmarks-website + # docker compose up -d --force-recreate vortex-bench-server + # d. Verify with `curl` against /health and a token-gated endpoint. \ No newline at end of file diff --git a/benchmarks-website/server/Cargo.toml b/benchmarks-website/server/Cargo.toml index 5b501adf0cc..07d1746a5e5 100644 --- a/benchmarks-website/server/Cargo.toml +++ b/benchmarks-website/server/Cargo.toml @@ -26,7 +26,8 @@ path = "src/main.rs" anyhow = { workspace = true } axum = "0.8" base64 = "0.22" -duckdb = { version = "1.4", features = ["bundled"] } +# track vortex-duckdb's bundled engine version (build.rs) +duckdb = { version = "1.10502", features = ["bundled"] } maud = { version = "0.27", features = ["axum"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/benchmarks-website/server/Dockerfile b/benchmarks-website/server/Dockerfile new file mode 100644 index 00000000000..81c2c4860b9 --- /dev/null +++ b/benchmarks-website/server/Dockerfile @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors +# +# Build context: repository root (the server is a workspace member). +# Build: docker build -f benchmarks-website/server/Dockerfile . +# Toolchain pinned to match rust-toolchain.toml. + +FROM rust:1.91-bookworm AS build + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build +COPY . . + +RUN cargo build --release -p vortex-bench-server --bin vortex-bench-server + +FROM debian:bookworm-slim + +# Keep this in lockstep with libduckdb-sys in Cargo.lock. +ARG DUCKDB_VERSION=1.5.2 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + libstdc++6 \ + unzip \ + wget \ + && wget -q "https://github.com/duckdb/duckdb/releases/download/v${DUCKDB_VERSION}/duckdb_cli-linux-aarch64.zip" -O /tmp/duckdb.zip \ + && unzip -q /tmp/duckdb.zip -d /usr/local/bin/ \ + && chmod +x /usr/local/bin/duckdb \ + && rm /tmp/duckdb.zip \ + && apt-get purge -y --auto-remove unzip wget \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /build/target/release/vortex-bench-server /usr/local/bin/vortex-bench-server + +WORKDIR /app/data + +EXPOSE 3000 + +CMD ["/usr/local/bin/vortex-bench-server"] diff --git a/benchmarks-website/server/scripts/backup.sh b/benchmarks-website/server/scripts/backup.sh new file mode 100755 index 00000000000..ca4a35f891f --- /dev/null +++ b/benchmarks-website/server/scripts/backup.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors +# +# Daily DuckDB backup for the vortex-bench-server v3 instance. +# Runs on the EC2 host via cron (see benchmarks-website/ec2-init.txt). +# +# Exports the running container's DuckDB to a local directory and uploads +# it to s3://vortex-ci-benchmark-results/v3-backups//. The instance +# IAM role already grants write access to that bucket (it is the same +# bucket cat-s3.sh uses for v2). +# +# At alpha this is a convenience backup: the data is also reproducible +# from CI dual-writes to the v3 ingest endpoint, so RPO is bounded by +# what CI has posted, not by this script's cadence. + +set -euo pipefail + +CONTAINER="${CONTAINER:-vortex-bench-server}" +DB_PATH="${DB_PATH:-/app/data/bench.duckdb}" +DATA_DIR="${DATA_DIR:-/opt/benchmarks-website/data}" +S3_PREFIX="${S3_PREFIX:-s3://vortex-ci-benchmark-results/v3-backups}" + +date_stamp="$(date -u +%Y%m%d)" +export_dir="backup-${date_stamp}" +host_export_dir="${DATA_DIR}/${export_dir}" + +# Run EXPORT DATABASE inside the container so we hit the same DuckDB +# build that wrote the file. The container path mirrors the host path +# under /app/data, so the export lands on the EBS volume. +docker exec "${CONTAINER}" \ + duckdb "${DB_PATH}" \ + -c "EXPORT DATABASE '/app/data/${export_dir}'" + +aws s3 cp \ + --recursive \ + "${host_export_dir}" \ + "${S3_PREFIX}/${date_stamp}/" + +# Keep the latest local export, drop older ones to bound disk use. +find "${DATA_DIR}" \ + -maxdepth 1 \ + -type d \ + -name "backup-*" \ + ! -path "${host_export_dir}" \ + -exec rm -rf {} + From 2eb6c73fee4b908b633029ea2b1a14d22f056b88 Mon Sep 17 00:00:00 2001 From: Connor Tsui <87130162+connortsui20@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:24:50 -0400 Subject: [PATCH 10/12] Benchmarks v3 migration to duckdb (#7646) This is a one-shot migration binary to take all of the data from `data.json.gz` and bring it into a duckdb database. Simply gathers and aggregates everything into memory and writes data in chunks with arrow arrays. Insert row-by-row took way too long, and the appender API in duckdb does not support `BIGINT[]` for some reason... --------- Signed-off-by: Claude Signed-off-by: Connor Tsui Co-authored-by: Claude Signed-off-by: Connor Tsui --- Cargo.lock | 60 ++ Cargo.toml | 1 + benchmarks-website/migrate/Cargo.toml | 41 + benchmarks-website/migrate/src/classifier.rs | 818 +++++++++++++++++ benchmarks-website/migrate/src/commits.rs | 100 +++ benchmarks-website/migrate/src/lib.rs | 21 + benchmarks-website/migrate/src/main.rs | 114 +++ benchmarks-website/migrate/src/migrate.rs | 836 ++++++++++++++++++ benchmarks-website/migrate/src/source.rs | 140 +++ benchmarks-website/migrate/src/v2.rs | 142 +++ benchmarks-website/migrate/src/verify.rs | 350 ++++++++ .../migrate/tests/classifier.rs | 439 +++++++++ .../migrate/tests/end_to_end.rs | 263 ++++++ 13 files changed, 3325 insertions(+) create mode 100644 benchmarks-website/migrate/Cargo.toml create mode 100644 benchmarks-website/migrate/src/classifier.rs create mode 100644 benchmarks-website/migrate/src/commits.rs create mode 100644 benchmarks-website/migrate/src/lib.rs create mode 100644 benchmarks-website/migrate/src/main.rs create mode 100644 benchmarks-website/migrate/src/migrate.rs create mode 100644 benchmarks-website/migrate/src/source.rs create mode 100644 benchmarks-website/migrate/src/v2.rs create mode 100644 benchmarks-website/migrate/src/verify.rs create mode 100644 benchmarks-website/migrate/tests/classifier.rs create mode 100644 benchmarks-website/migrate/tests/end_to_end.rs diff --git a/Cargo.lock b/Cargo.lock index d8b13b8ae20..fbf7c8dbcfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3747,6 +3747,7 @@ dependencies = [ "fallible-streaming-iterator", "hashlink", "libduckdb-sys", + "num", "num-integer", "rust_decimal", "strum 0.27.2", @@ -6354,6 +6355,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -6389,6 +6404,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -10344,6 +10381,29 @@ dependencies = [ "vortex-tensor", ] +[[package]] +name = "vortex-bench-migrate" +version = "0.1.0-alpha.0" +dependencies = [ + "anyhow", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-schema 58.1.0", + "clap", + "duckdb", + "flate2", + "reqwest 0.13.3", + "rstest", + "serde", + "serde_json", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "vortex-bench-server", + "vortex-utils", +] + [[package]] name = "vortex-bench-server" version = "0.1.0-alpha.0" diff --git a/Cargo.toml b/Cargo.toml index a82809c9a40..d56fc893658 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ members = [ "benchmarks/vector-search-bench", # Benchmarks website v3 (alpha) - leaf binary, not part of vortex-* API "benchmarks-website/server", + "benchmarks-website/migrate", ] exclude = ["java/testfiles", "wasm-test"] resolver = "2" diff --git a/benchmarks-website/migrate/Cargo.toml b/benchmarks-website/migrate/Cargo.toml new file mode 100644 index 00000000000..45a752df397 --- /dev/null +++ b/benchmarks-website/migrate/Cargo.toml @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright the Vortex contributors + +[package] +name = "vortex-bench-migrate" +version = "0.1.0-alpha.0" +edition = "2024" +rust-version = "1.91.0" +license = "Apache-2.0" +description = "One-shot historical migrator from the v2 benchmarks S3 dataset to a v3 DuckDB file" +publish = false + +[[bin]] +name = "vortex-bench-migrate" +path = "src/main.rs" + +# Throwaway binary, not part of the vortex-* public API surface. +# Errors use anyhow, and the crate is intentionally outside the +# workspace public-api lockfile set. + +[dependencies] +anyhow = { workspace = true } +arrow-array = { workspace = true } +arrow-buffer = { workspace = true } +arrow-schema = { workspace = true } +clap = { workspace = true, features = ["derive"] } +# track vortex-duckdb's bundled engine version (build.rs) +duckdb = { version = "1.10502", features = ["bundled", "appender-arrow"] } +flate2 = "1.1" +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing = { workspace = true, features = ["std"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } +vortex-bench-server = { path = "../server" } +vortex-utils = { workspace = true } + +[dev-dependencies] +rstest = { workspace = true } +tempfile = { workspace = true } diff --git a/benchmarks-website/migrate/src/classifier.rs b/benchmarks-website/migrate/src/classifier.rs new file mode 100644 index 00000000000..8a17b31fcd2 --- /dev/null +++ b/benchmarks-website/migrate/src/classifier.rs @@ -0,0 +1,818 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Bug-for-bug port of v2's `getGroup`, `formatQuery`, and +//! `normalizeChartName` from `benchmarks-website/server.js`, plus the +//! mapping from v2 group + name pattern to a v3 fact-table bin. +//! +//! The v2 classifier was the source of truth for what historical +//! records mean. It groups records by name prefix into one of: +//! "Random Access", "Compression", "Compression Size", or one of the +//! SQL query suites (with optional fan-out by storage and scale +//! factor for TPC-H/TPC-DS). This module reproduces that logic and +//! then hops to a v3 fact-table bin, since v3 stores dim values as +//! columns instead of name fragments. +//! +//! Engine and format strings stored in v3 columns are pulled from the +//! raw, pre-rename v2 record name. v2's `ENGINE_RENAMES` was a v2 +//! read-time UI concern (e.g. `vortex-file-compressed` rendered as +//! `vortex` and `parquet-tokio-local-disk` rendered as `parquet-nvme`). +//! v3 stores canonical `Format::name()` strings to match what the v3 +//! live emitter writes, so historical and live records share series. + +use crate::v2::V2Record; +use crate::v2::dataset_scale_factor; + +/// Static port of v2's `QUERY_SUITES`. +pub const QUERY_SUITES: &[QuerySuite] = &[ + QuerySuite { + prefix: "clickbench", + display_name: "Clickbench", + query_prefix: "CLICKBENCH", + dataset_key: None, + fan_out: false, + skip: false, + }, + QuerySuite { + prefix: "statpopgen", + display_name: "Statistical and Population Genetics", + query_prefix: "STATPOPGEN", + dataset_key: None, + fan_out: false, + skip: false, + }, + QuerySuite { + prefix: "polarsignals", + display_name: "PolarSignals Profiling", + query_prefix: "POLARSIGNALS", + dataset_key: None, + fan_out: false, + skip: false, + }, + QuerySuite { + prefix: "gharchive", + display_name: "GhArchive", + query_prefix: "GHARCHIVE", + dataset_key: None, + fan_out: false, + skip: false, + }, + QuerySuite { + prefix: "tpch", + display_name: "TPC-H", + query_prefix: "TPC-H", + dataset_key: Some("tpch"), + fan_out: true, + skip: false, + }, + QuerySuite { + prefix: "tpcds", + display_name: "TPC-DS", + query_prefix: "TPC-DS", + dataset_key: Some("tpcds"), + fan_out: true, + skip: false, + }, + QuerySuite { + prefix: "fineweb", + display_name: "Fineweb", + query_prefix: "FINEWEB", + dataset_key: None, + fan_out: false, + skip: false, + }, +]; + +/// Static port of v2's `ENGINE_RENAMES`. Applied to the "series" half +/// of a benchmark name (the part after the first `/`) before splitting +/// on `:` into engine/format. Order doesn't matter — keys are unique. +const ENGINE_RENAMES: &[(&str, &str)] = &[ + ("datafusion:vortex-file-compressed", "datafusion:vortex"), + ("datafusion:parquet", "datafusion:parquet"), + ("datafusion:arrow", "datafusion:in-memory-arrow"), + ("datafusion:lance", "datafusion:lance"), + ("datafusion:vortex-compact", "datafusion:vortex-compact"), + ("duckdb:vortex-file-compressed", "duckdb:vortex"), + ("duckdb:parquet", "duckdb:parquet"), + ("duckdb:duckdb", "duckdb:duckdb"), + ("duckdb:vortex-compact", "duckdb:vortex-compact"), + ("vortex-tokio-local-disk", "vortex-nvme"), + ("vortex-compact-tokio-local-disk", "vortex-compact-nvme"), + ("lance-tokio-local-disk", "lance-nvme"), + ("parquet-tokio-local-disk", "parquet-nvme"), + ("lance", "lance"), +]; + +/// One entry of `QUERY_SUITES`. +#[derive(Debug, Clone, Copy)] +pub struct QuerySuite { + pub prefix: &'static str, + pub display_name: &'static str, + pub query_prefix: &'static str, + pub dataset_key: Option<&'static str>, + pub fan_out: bool, + pub skip: bool, +} + +/// Group a v2 record falls into. Mirrors `getGroup` in `server.js`, +/// including the fan-out group naming for TPC-H/TPC-DS. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum V2Group { + RandomAccess, + Compression, + CompressionSize, + Query { + suite_index: usize, + /// `Some` for fan-out suites only. + storage: Option, + /// `Some` for fan-out suites only. + scale_factor: Option, + }, +} + +impl V2Group { + /// Display name as v2 served it from `/api/metadata`. + pub fn display_name(&self) -> String { + match self { + V2Group::RandomAccess => "Random Access".into(), + V2Group::Compression => "Compression".into(), + V2Group::CompressionSize => "Compression Size".into(), + V2Group::Query { + suite_index, + storage, + scale_factor, + } => { + let suite = &QUERY_SUITES[*suite_index]; + if let (Some(storage), Some(sf)) = (storage, scale_factor) { + format!("{} ({}) (SF={})", suite.display_name, storage, sf) + } else { + suite.display_name.to_string() + } + } + } + } +} + +/// Apply v2's `ENGINE_RENAMES`. Reproduces the JS `rename`: +/// `RENAMES[s.toLowerCase()] || RENAMES[s] || s`. +pub fn rename_engine(s: &str) -> String { + let lower = s.to_lowercase(); + for (k, v) in ENGINE_RENAMES { + if *k == lower { + return (*v).to_string(); + } + } + for (k, v) in ENGINE_RENAMES { + if *k == s { + return (*v).to_string(); + } + } + s.to_string() +} + +/// Faithful port of v2's `formatQuery`: maps `clickbench_q07` → +/// `"CLICKBENCH Q7"`. Returns the original (uppercased, +/// `-` and `_` replaced with spaces) when no suite matches. +pub fn format_query(q: &str) -> String { + let lower = q.to_lowercase(); + for suite in QUERY_SUITES { + if suite.skip { + continue; + } + let prefix = suite.prefix; + if let Some(rest) = lower.strip_prefix(prefix) + && let Some(idx) = parse_query_index(rest) + { + return format!("{} Q{}", suite.query_prefix, idx); + } + } + let mut out = q.to_uppercase(); + out = out.replace(['_', '-'], " "); + out +} + +/// Parse the `_q07` / ` q7` / `q42` tail used by `format_query`. +/// Returns the integer query index if the tail matches the v2 regex +/// `^[_ ]?q(\d+)`. +fn parse_query_index(rest: &str) -> Option { + let after_sep = rest + .strip_prefix('_') + .or_else(|| rest.strip_prefix(' ')) + .unwrap_or(rest); + let after_q = after_sep + .strip_prefix('q') + .or_else(|| after_sep.strip_prefix('Q'))?; + let digits: String = after_q.chars().take_while(|c| c.is_ascii_digit()).collect(); + if digits.is_empty() { + return None; + } + digits.parse().ok() +} + +/// Faithful port of v2's `normalizeChartName`. +pub fn normalize_chart_name(group: &V2Group, chart_name: &str) -> String { + if matches!(group, V2Group::CompressionSize) && chart_name == "VORTEX FILE COMPRESSED SIZE" { + return "VORTEX SIZE".into(); + } + chart_name.to_string() +} + +/// Port of v2's `getGroup`. Returns `None` for skipped suites +/// (e.g. `fineweb`) or names that match nothing. +pub fn get_group(record: &V2Record) -> Option { + let lower = record.name.to_lowercase(); + + if lower.starts_with("random-access/") || lower.starts_with("random access/") { + return Some(V2Group::RandomAccess); + } + + if lower.starts_with("vortex size/") + || lower.starts_with("vortex-file-compressed size/") + || lower.starts_with("parquet size/") + || lower.starts_with("parquet-zstd size/") + || lower.starts_with("lance size/") + || lower.contains(":raw size/") + || lower.contains(":parquet-zstd size/") + || lower.contains(":lance size/") + { + return Some(V2Group::CompressionSize); + } + + if lower.starts_with("compress time/") + || lower.starts_with("decompress time/") + || lower.starts_with("parquet_rs-zstd compress") + || lower.starts_with("parquet_rs-zstd decompress") + || lower.starts_with("lance compress") + || lower.starts_with("lance decompress") + || lower.starts_with("vortex:lance ratio") + || lower.starts_with("vortex:parquet-zstd ratio") + // Typo'd v2 emitter wrote `parquet-zst` (no `d`) for some + // ratio records; match both spellings so they classify as + // derived ratios instead of falling through to Unknown. + || lower.starts_with("vortex:parquet-zst ratio") + || lower.starts_with("vortex:raw ratio") + { + return Some(V2Group::Compression); + } + + for (i, suite) in QUERY_SUITES.iter().enumerate() { + let prefix_q = format!("{}_q", suite.prefix); + let prefix_slash = format!("{}/", suite.prefix); + if !lower.starts_with(&prefix_q) && !lower.starts_with(&prefix_slash) { + continue; + } + if suite.skip { + return None; + } + if !suite.fan_out { + return Some(V2Group::Query { + suite_index: i, + storage: None, + scale_factor: None, + }); + } + let storage = match record.storage.as_deref().map(str::to_uppercase).as_deref() { + Some("S3") => "S3", + _ => "NVMe", + }; + let dataset_key = suite.dataset_key.unwrap_or(suite.prefix); + let raw_sf = record + .dataset + .as_ref() + .and_then(|d| dataset_scale_factor(d, dataset_key)); + let sf = raw_sf + .as_deref() + .and_then(|s| s.parse::().ok()) + .map(|f| f.round() as i64) + .unwrap_or(1); + return Some(V2Group::Query { + suite_index: i, + storage: Some(storage.into()), + scale_factor: Some(sf.to_string()), + }); + } + + None +} + +/// Group + chart + series breakdown for a v2 record, using the same +/// rules `server.js` applies in `refresh()`. Equivalent to v2's +/// `(group, chartName, seriesName)` triple after rename / skip rules. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct V2Classification { + pub group: V2Group, + pub chart: String, + pub series: String, +} + +/// Apply the same chart / series naming v2's `refresh()` does, plus +/// the throughput / `PARQUET-UNC` skip rules. +pub fn classify_v2(record: &V2Record) -> Option { + if record.name.contains(" throughput") { + return None; + } + let group = get_group(record)?; + let parts: Vec<&str> = record.name.split('/').collect(); + let (chart, series) = match (&group, parts.len()) { + (V2Group::RandomAccess, 4) => { + let chart = format!("{}/{}", parts[1], parts[2]) + .to_uppercase() + .replace(['_', '-'], " "); + let series = rename_engine(if parts[3].is_empty() { + "default" + } else { + parts[3] + }); + (chart, series) + } + (V2Group::RandomAccess, 2) => ( + "RANDOM ACCESS".to_string(), + rename_engine(if parts[1].is_empty() { + "default" + } else { + parts[1] + }), + ), + (V2Group::RandomAccess, _) => return None, + _ => { + let series_raw = if parts.len() >= 2 && !parts[1].is_empty() { + parts[1] + } else { + "default" + }; + let series = rename_engine(series_raw); + let chart = format_query(parts[0]); + (chart, series) + } + }; + let chart = normalize_chart_name(&group, &chart); + if chart.contains("PARQUET-UNC") { + return None; + } + Some(V2Classification { + group, + chart, + series, + }) +} + +/// Mapping target: which v3 fact table a v2 record lands in, plus the +/// dim values that table needs. +#[derive(Debug, Clone, PartialEq)] +pub enum V3Bin { + Query { + dataset: String, + dataset_variant: Option, + scale_factor: Option, + query_idx: i32, + storage: String, + engine: String, + format: String, + }, + CompressionTime { + dataset: String, + dataset_variant: Option, + format: String, + op: String, + }, + CompressionSize { + dataset: String, + dataset_variant: Option, + format: String, + }, + RandomAccess { + dataset: String, + format: String, + }, +} + +/// Top-level entry point. Combines `classify_v2` with the v3 fact-table +/// mapping. Returns `None` for records that: +/// +/// - Don't match any v2 group (uncategorized prefix). +/// - Are explicitly skipped by v2 (throughput, PARQUET-UNC, fineweb). +/// - Are computed-at-read-time ratios that v3 derives from +/// `compression_sizes` (`vortex:parquet-zstd ratio …`, +/// `vortex:lance ratio …`, `vortex:raw ratio …`, +/// `vortex:* size/…`). +pub fn classify(record: &V2Record) -> Option { + let cls = classify_v2(record)?; + match &cls.group { + V2Group::RandomAccess => bin_random_access(&cls, record), + V2Group::Compression => bin_compression_time(&cls, record), + V2Group::CompressionSize => bin_compression_size(&cls, record), + V2Group::Query { .. } => bin_query(&cls, record), + } +} + +/// Reason the classifier dropped a record. Intentional skips (v2 +/// patterns v3 deliberately doesn't store) are NOT errors; they don't +/// count against the uncategorized gate. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Skip { + /// `vortex:* ratio …` and `vortex:* size` — derived in v3 from + /// `compression_sizes` joined to itself. + DerivedRatio, + /// `throughput` records — v2 derived these from latencies. + Throughput, + /// A v2 query suite marked `skip: true` in QUERY_SUITES. + SkippedSuite, + /// random-access record with an unsupported part count. + UnsupportedShape, + /// Record had no `value` field. + NoValue, + /// Dim outside the v3 emitter's allowlist (e.g. `parquet-zstd`, + /// historical-only suites no longer in CI). + Deprecated, + /// v2 memory measurements (`*_memory/*` records). Carry top-level + /// `peak_physical_memory` / `peak_virtual_memory` / + /// `physical_memory_delta` / `virtual_memory_delta` fields that + /// `V2Record` doesn't deserialize. Not migrated for alpha; merging + /// into the corresponding QueryMeasurement row is future work. + HistoricalMemory, +} + +/// Engines the v3 emitter produces today. Anything else is historical +/// and gets bucketed as `Skip::Deprecated`. +/// +/// ORCHESTRATOR NOTE: confirm against `vortex-bench`'s `Engine` enum +/// before handing off; edit if the live set differs. +const V3_ENGINES: &[&str] = &["datafusion", "duckdb", "vortex", "arrow"]; + +/// Formats the v3 emitter produces today (`Format::name()` values). +/// +/// ORCHESTRATOR NOTE: confirm against `vortex-bench/src/lib.rs` +/// `Format::name()` before handing off. +const V3_FORMATS: &[&str] = &[ + "vortex-file-compressed", + "vortex-compact", + "parquet", + "lance", + "csv", + "arrow", + "duckdb", +]; + +/// Query suites the v3 CI runs today. Suites outside this list still +/// classify (so historical analyses stay coherent) but get bucketed +/// as `Skip::Deprecated` so they don't render as orphan charts in v3. +/// +/// `fineweb` is included because `.github/workflows/sql-benchmarks.yml` +/// still has `fineweb` and `fineweb-s3` matrix entries. `gharchive` +/// stays excluded — it's defined in `vortex-bench` but no current +/// workflow runs it. +const V3_QUERY_SUITES: &[&str] = &[ + "clickbench", + "tpch", + "tpcds", + "statpopgen", + "polarsignals", + "fineweb", +]; + +/// Returns true if every dim that v3 stores as a column is on the +/// emitter's current allowlist. Dim values outside the allowlist mean +/// historical-only formats / engines that the v3 UI has nothing to +/// render against. +fn is_v3_dim(bin: &V3Bin) -> bool { + match bin { + V3Bin::Query { engine, format, .. } => { + V3_ENGINES.contains(&engine.as_str()) && V3_FORMATS.contains(&format.as_str()) + } + V3Bin::CompressionTime { format, .. } + | V3Bin::CompressionSize { format, .. } + | V3Bin::RandomAccess { format, .. } => V3_FORMATS.contains(&format.as_str()), + } +} + +/// Outcome of running the classifier on a v2 record. Distinguishes +/// "we know we don't want this" (`Skip`) from "we don't recognize this" +/// (`Unknown`); the migrator's 5% gate fires only on the latter. +#[derive(Debug, Clone)] +pub enum Outcome { + Bin(V3Bin), + Skip(Skip), + Unknown, +} + +/// Like [`classify`], but reports *why* a record was dropped. Intended +/// for the migrator so the 5% uncategorized gate doesn't trip on +/// records v2 deliberately doesn't render (ratios, throughput, +/// skipped suites). +pub fn classify_outcome(record: &V2Record) -> Outcome { + if record.name.contains(" throughput") { + return Outcome::Skip(Skip::Throughput); + } + // v2 memory records: e.g. "clickbench_q07_memory/datafusion:parquet". + // Match the `_memory/` infix BEFORE the engine/format split, so they + // route to a known Skip variant instead of slipping through to + // Outcome::Unknown and tripping the 5% gate. + let lower = record.name.to_lowercase(); + if let Some((head, _)) = lower.split_once('/') + && head.ends_with("_memory") + { + return Outcome::Skip(Skip::HistoricalMemory); + } + let Some(group) = get_group(record) else { + return Outcome::Unknown; + }; + if let V2Group::Query { suite_index, .. } = &group + && QUERY_SUITES[*suite_index].skip + { + return Outcome::Skip(Skip::SkippedSuite); + } + let Some(cls) = classify_v2(record) else { + // get_group succeeded but classify_v2 didn't — shape mismatch. + return Outcome::Skip(Skip::UnsupportedShape); + }; + let derived = match &cls.group { + V2Group::Compression => { + let lc = cls.chart.to_lowercase(); + lc.contains("ratio") || lc.contains(':') + } + V2Group::CompressionSize => cls.chart.to_lowercase().contains(':'), + _ => false, + }; + if derived { + return Outcome::Skip(Skip::DerivedRatio); + } + let bin = match &cls.group { + V2Group::RandomAccess => bin_random_access(&cls, record), + V2Group::Compression => bin_compression_time(&cls, record), + V2Group::CompressionSize => bin_compression_size(&cls, record), + V2Group::Query { .. } => bin_query(&cls, record), + }; + let Some(bin) = bin else { + return Outcome::Unknown; + }; + if !is_v3_dim(&bin) { + return Outcome::Skip(Skip::Deprecated); + } + if let V2Group::Query { suite_index, .. } = &group + && !V3_QUERY_SUITES.contains(&QUERY_SUITES[*suite_index].prefix) + { + return Outcome::Skip(Skip::Deprecated); + } + Outcome::Bin(bin) +} + +fn bin_random_access(cls: &V2Classification, record: &V2Record) -> Option { + // v2 chart name shape: "RANDOM ACCESS" or "DATASET/PATTERN" (uppercase). + // We store it as the v3 dataset value verbatim, lowercased so + // `/api/groups` returns canonical lowercase names. + let dataset = cls.chart.to_lowercase(); + if dataset.is_empty() { + return None; + } + // Pull format from the raw, pre-rename v2 name so v3 stores the + // canonical `Format::name()` string (matching what the v3 live + // emitter writes). Raw shape is + // `random-access///-tokio-local-disk` + // (4-part) or `random-access/-tokio-local-disk` (2-part + // legacy). After stripping the `-tokio-local-disk` suffix, map the + // v2 random-access ext label (`vortex`, from `Format::ext()`) to + // the canonical name (`vortex-file-compressed`, from + // `Format::name()`). `parquet` and `lance` match between ext and + // name. The `vortex` ext is shared by both `OnDiskVortex` (name + // `vortex-file-compressed`) and `VortexCompact` (name + // `vortex-compact`), but v2's random-access bench only emitted + // `OnDiskVortex`, so mapping to `vortex-file-compressed` is + // correct for all historical data. + let parts: Vec<&str> = record.name.split('/').collect(); + let raw = match parts.len() { + 4 => parts[3], + 2 => parts[1], + _ => return None, + }; + if raw.is_empty() || raw == "default" { + return None; + } + let stripped = raw.strip_suffix("-tokio-local-disk").unwrap_or(raw); + let format = match stripped { + "vortex" => "vortex-file-compressed".to_string(), + other => other.to_lowercase(), + }; + Some(V3Bin::RandomAccess { dataset, format }) +} + +fn bin_compression_time(cls: &V2Classification, _record: &V2Record) -> Option { + // v2 compression chart names look like (after format_query): + // "COMPRESS TIME" [vortex/encode] + // "DECOMPRESS TIME" [vortex/decode] + // "PARQUET RS ZSTD COMPRESS TIME" [parquet/encode] + // "PARQUET RS ZSTD DECOMPRESS TIME" [parquet/decode] + // "LANCE COMPRESS TIME" [lance/encode] + // "LANCE DECOMPRESS TIME" [lance/decode] + // "VORTEX:LANCE RATIO COMPRESS TIME" [drop] + // "VORTEX:PARQUET-ZSTD RATIO COMPRESS TIME" [drop] + // "VORTEX:RAW RATIO COMPRESS TIME" [drop] + let lc = cls.chart.to_lowercase(); + if lc.contains("ratio") || lc.contains(':') { + // Ratios are computed at read time from compression_sizes. + return None; + } + let (format, op) = if lc.starts_with("compress time") { + ("vortex-file-compressed", "encode") + } else if lc.starts_with("decompress time") { + ("vortex-file-compressed", "decode") + } else if lc.starts_with("parquet rs zstd compress time") { + ("parquet", "encode") + } else if lc.starts_with("parquet rs zstd decompress time") { + ("parquet", "decode") + } else if lc.starts_with("lance compress time") { + ("lance", "encode") + } else if lc.starts_with("lance decompress time") { + ("lance", "decode") + } else { + return None; + }; + let dataset = cls.series.to_lowercase(); + if dataset.is_empty() || dataset == "default" { + return None; + } + Some(V3Bin::CompressionTime { + dataset, + dataset_variant: None, + format: format.to_string(), + op: op.to_string(), + }) +} + +fn bin_compression_size(cls: &V2Classification, record: &V2Record) -> Option { + let lc = cls.chart.to_lowercase(); + // Ratios like "VORTEX:PARQUET ZSTD SIZE" / "VORTEX:LANCE SIZE" / + // "VORTEX:RAW SIZE" are derived from compression_sizes at read + // time, not stored. + if lc.contains(':') { + return None; + } + // `parquet-zstd size` shares a leading "parquet" with `parquet size`, + // so check the more specific prefix first. `format_query` upper-cases + // and replaces `-`/`_` with spaces, so the chart we match against is + // `"PARQUET ZSTD SIZE"` (no hyphen) — same convention as the existing + // `"parquet rs zstd compress time"` branches above. + let format = if lc.starts_with("vortex size") { + "vortex-file-compressed" + } else if lc.starts_with("parquet zstd size") { + "parquet-zstd" + } else if lc.starts_with("parquet size") { + "parquet" + } else if lc.starts_with("lance size") { + "lance" + } else { + return None; + }; + let dataset = cls.series.to_lowercase(); + if dataset.is_empty() || dataset == "default" { + return None; + } + // Mirror the file-sizes ingest path's dataset_variant derivation + // (see `migrate::migrate_file_sizes`): pull the SF out of the v2 + // record's `dataset` object when present, drop empty / "1.0". + // Without this both code paths produce the same `mid` only by + // accident, so SF=10 file-sizes rows wouldn't merge with the + // matching data.json.gz "vortex size/tpch" rows. + let dataset_variant = record + .dataset + .as_ref() + .and_then(|d| crate::v2::dataset_scale_factor(d, dataset.as_str())) + .filter(|s| !s.is_empty() && s.as_str() != "1.0"); + Some(V3Bin::CompressionSize { + dataset, + dataset_variant, + format: format.to_string(), + }) +} + +fn bin_query(cls: &V2Classification, record: &V2Record) -> Option { + let V2Group::Query { + suite_index, + storage, + scale_factor, + } = &cls.group + else { + return None; + }; + let suite = &QUERY_SUITES[*suite_index]; + + // Pull the query index from the *raw* name's first part instead of + // the formatted chart, so we don't have to round-trip "Q07". + let raw_first = record.name.split('/').next().unwrap_or(""); + let query_idx = parse_query_index_from_first(raw_first)?; + + // Pull engine:format from the raw, pre-rename second segment so v3 + // stores canonical `Format::name()` strings (e.g. + // `vortex-file-compressed`) that match what the v3 live emitter + // writes. `cls.series` has been through v2's `ENGINE_RENAMES` for + // UI display and is not appropriate for v3 columns. + // + // Older v2 records emitted display-case engines (e.g. `DataFusion`, + // `DuckDB`); newer ones emit lowercase. Lowercase here so dedup + // collapses both spellings into a single canonical row. + let raw_series = record.name.split('/').nth(1)?; + let (engine, format) = split_engine_format(raw_series)?; + let engine = engine.to_lowercase(); + let format = format.to_lowercase(); + + let storage_v3 = match storage.as_deref() { + Some("S3") => "s3".to_string(), + Some("NVMe") => "nvme".to_string(), + _ => "nvme".to_string(), + }; + + // ClickBench's "flavor" lives in dataset_variant per benchmark-mapping.md + // - we don't have it from a v2 name string, so we leave it None. + Some(V3Bin::Query { + dataset: suite.prefix.to_string(), + dataset_variant: None, + scale_factor: scale_factor.clone(), + query_idx, + storage: storage_v3, + engine, + format, + }) +} + +/// Pull the integer query index out of the leading name part, which is +/// always `_q` or ` q` for SQL query records. +fn parse_query_index_from_first(first: &str) -> Option { + let lower = first.to_lowercase(); + for suite in QUERY_SUITES { + if let Some(rest) = lower.strip_prefix(suite.prefix) + && let Some(idx) = parse_query_index(rest) + { + return Some(idx as i32); + } + } + None +} + +/// Split a renamed series like `datafusion:parquet` into +/// `(engine, format)`. Returns `None` for series with no `:` since +/// v3 requires both columns. +fn split_engine_format(series: &str) -> Option<(String, String)> { + let mut split = series.splitn(2, ':'); + let engine = split.next()?.trim().to_string(); + let format = split.next()?.trim().to_string(); + if engine.is_empty() || format.is_empty() { + return None; + } + Some((engine, format)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn record(name: &str) -> V2Record { + V2Record { + name: name.to_string(), + commit_id: Some("deadbeef".into()), + unit: None, + value: None, + storage: None, + dataset: None, + all_runtimes: None, + env_triple: None, + } + } + + #[test] + fn format_query_round_trips() { + assert_eq!(format_query("clickbench_q07"), "CLICKBENCH Q7"); + assert_eq!(format_query("tpch_q01"), "TPC-H Q1"); + assert_eq!(format_query("tpcds_q42"), "TPC-DS Q42"); + assert_eq!(format_query("statpopgen_q3"), "STATPOPGEN Q3"); + assert_eq!(format_query("foo bar"), "FOO BAR"); + } + + #[test] + fn rename_engine_canonicalizes_disk_names() { + assert_eq!(rename_engine("vortex-tokio-local-disk"), "vortex-nvme"); + assert_eq!( + rename_engine("datafusion:vortex-file-compressed"), + "datafusion:vortex" + ); + assert_eq!(rename_engine("unknown-engine"), "unknown-engine"); + } + + #[test] + fn parse_query_index_handles_separators() { + assert_eq!(parse_query_index("_q07"), Some(7)); + assert_eq!(parse_query_index(" q7"), Some(7)); + assert_eq!(parse_query_index("q42"), Some(42)); + assert_eq!(parse_query_index("xq7"), None); + } + + #[test] + fn random_access_bins_dataset_pattern() { + let bin = classify(&record("random-access/taxi/take/parquet")).unwrap(); + assert_eq!( + bin, + V3Bin::RandomAccess { + dataset: "taxi/take".into(), + format: "parquet".into(), + } + ); + } +} diff --git a/benchmarks-website/migrate/src/commits.rs b/benchmarks-website/migrate/src/commits.rs new file mode 100644 index 00000000000..28d63a5bd19 --- /dev/null +++ b/benchmarks-website/migrate/src/commits.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Commit upserts. Adapts a [`crate::v2::V2Commit`] into the v3 +//! `commits` row shape (a [`vortex_bench_server::records::CommitInfo`]). + +use anyhow::Context as _; +use anyhow::Result; +use duckdb::Transaction; +use duckdb::params; + +use crate::v2::V2Commit; + +/// Insert a v3 `commits` row for one v2 commit. Missing fields are +/// filled with the empty string, matching the v3 schema's `NOT NULL` +/// constraints; the call site logs a warning for each fallback so +/// the operator can spot bad inputs. +pub fn upsert_commit(tx: &Transaction<'_>, commit: &V2Commit) -> Result { + let mut warnings = Vec::new(); + let timestamp = require_field(&commit.timestamp, "timestamp", &commit.id, &mut warnings); + let message = require_field(&commit.message, "message", &commit.id, &mut warnings); + let author_name = require_field( + &commit.author.as_ref().and_then(|p| p.name.clone()), + "author.name", + &commit.id, + &mut warnings, + ); + let author_email = require_field( + &commit.author.as_ref().and_then(|p| p.email.clone()), + "author.email", + &commit.id, + &mut warnings, + ); + let committer_name = require_field( + &commit.committer.as_ref().and_then(|p| p.name.clone()), + "committer.name", + &commit.id, + &mut warnings, + ); + let committer_email = require_field( + &commit.committer.as_ref().and_then(|p| p.email.clone()), + "committer.email", + &commit.id, + &mut warnings, + ); + let tree_sha = require_field(&commit.tree_id, "tree_id", &commit.id, &mut warnings); + let url = require_field(&commit.url, "url", &commit.id, &mut warnings); + + tx.execute( + r#" + INSERT INTO commits ( + commit_sha, timestamp, message, author_name, author_email, + committer_name, committer_email, tree_sha, url + ) VALUES (?, CAST(? AS TIMESTAMPTZ), ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (commit_sha) DO UPDATE SET + timestamp = excluded.timestamp, + message = excluded.message, + author_name = excluded.author_name, + author_email = excluded.author_email, + committer_name = excluded.committer_name, + committer_email = excluded.committer_email, + tree_sha = excluded.tree_sha, + url = excluded.url + "#, + params![ + commit.id, + timestamp, + message, + author_name, + author_email, + committer_name, + committer_email, + tree_sha, + url, + ], + ) + .with_context(|| format!("upserting commit {}", commit.id))?; + Ok(UpsertOutcome { warnings }) +} + +fn require_field( + field: &Option, + name: &str, + sha: &str, + warnings: &mut Vec, +) -> String { + match field { + Some(s) => s.clone(), + None => { + warnings.push(format!("commit {sha} missing {name}")); + String::new() + } + } +} + +/// Per-call warning bag returned to the caller for logging. +#[derive(Debug, Default)] +pub struct UpsertOutcome { + pub warnings: Vec, +} diff --git a/benchmarks-website/migrate/src/lib.rs b/benchmarks-website/migrate/src/lib.rs new file mode 100644 index 00000000000..5e8d9c64907 --- /dev/null +++ b/benchmarks-website/migrate/src/lib.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! One-shot historical migrator from v2's S3-hosted benchmark dataset +//! to a v3 DuckDB file. +//! +//! The v2 dataset is JSONL of bare benchmark records keyed by name string. +//! v3 uses five typed fact tables with explicit dim columns. This crate +//! ports v2's `getGroup` classifier (in `benchmarks-website/server.js`) +//! bug-for-bug so that historical rows survive the migration with the +//! same group / chart / series structure as the live v2 server. +//! +//! The migrator is throwaway: once v3 cuts over, both the binary and +//! the classifier go away. + +pub mod classifier; +pub mod commits; +pub mod migrate; +pub mod source; +pub mod v2; +pub mod verify; diff --git a/benchmarks-website/migrate/src/main.rs b/benchmarks-website/migrate/src/main.rs new file mode 100644 index 00000000000..366834ed441 --- /dev/null +++ b/benchmarks-website/migrate/src/main.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! `vortex-bench-migrate` CLI: a one-shot historical migrator from +//! v2's S3 dataset into a v3 DuckDB file, plus a structural diff +//! against the live v2 `/api/metadata` endpoint for spotting +//! classifier regressions. + +use std::path::PathBuf; +use std::process::ExitCode; + +use anyhow::Context as _; +use anyhow::Result; +use clap::Parser; +use clap::Subcommand; +use clap::ValueEnum; +use tracing_subscriber::EnvFilter; +use vortex_bench_migrate::migrate; +use vortex_bench_migrate::source::Source; +use vortex_bench_migrate::verify; + +/// One-shot historical migrator from v2's S3 dataset to v3 DuckDB. +#[derive(Debug, Parser)] +#[command(name = "vortex-bench-migrate", version, about)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Read v2's data.json.gz / commits.json / file-sizes-*.json.gz + /// and write a fully populated v3 DuckDB at `--output`. + Run { + /// Path to write the v3 DuckDB to. Created if absent. + #[arg(long)] + output: PathBuf, + /// Where to fetch v2 dumps from. + #[arg(long, value_enum, default_value_t = SourceKind::PublicS3)] + source: SourceKind, + /// For `--source=local`, the directory containing + /// `data.json.gz`, `commits.json`, and `file-sizes-*.json.gz`. + #[arg(long, required_if_eq("source", "local"))] + source_dir: Option, + }, + /// Diff a migrated DuckDB against the live v2 `/api/metadata` + /// endpoint. Exits 0 if every v2 group is present in v3, 1 + /// otherwise so this can gate a CI step. + Verify { + /// HTTPS root of a running v2 server (e.g. `https://bench.vortex.dev`). + #[arg(long)] + against: String, + /// Path to the migrated v3 DuckDB. + #[arg(long)] + duckdb: PathBuf, + }, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +enum SourceKind { + PublicS3, + Local, +} + +fn main() -> ExitCode { + if let Err(err) = run() { + eprintln!("error: {err:#}"); + return ExitCode::from(2); + } + ExitCode::SUCCESS +} + +fn run() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_env("VORTEX_BENCH_LOG").unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + match cli.command { + Command::Run { + output, + source, + source_dir, + } => { + let source = match source { + SourceKind::PublicS3 => Source::PublicS3, + SourceKind::Local => { + Source::Local(source_dir.context("--source=local requires --source-dir")?) + } + }; + let summary = migrate::run(&source, &output)?; + print!("{summary}"); + if summary.uncategorized_fraction() > 0.05 { + anyhow::bail!( + "uncategorized records ({:.2}%) exceed the 5% gate; \ + stop and report unmatched prefixes (see summary above) \ + before proceeding", + 100.0 * summary.uncategorized_fraction() + ); + } + Ok(()) + } + Command::Verify { against, duckdb } => { + let report = verify::run(&against, &duckdb)?; + print!("{report}"); + if !report.v2_groups_covered() { + std::process::exit(1); + } + Ok(()) + } + } +} diff --git a/benchmarks-website/migrate/src/migrate.rs b/benchmarks-website/migrate/src/migrate.rs new file mode 100644 index 00000000000..7b3b32bb51c --- /dev/null +++ b/benchmarks-website/migrate/src/migrate.rs @@ -0,0 +1,836 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! End-to-end migration of one v2 dataset into a v3 DuckDB file. +//! +//! Streams `data.json.gz` line-by-line, runs each record through the +//! [`classifier`], and writes one row per record into the appropriate v3 fact table. +//! Every row's `measurement_id` is computed via the server's `measurement_id_*` functions so the +//! result is byte-compatible with what fresh `/api/ingest` would have produced. +//! +//! Bulk-load shape: rows are accumulated in memory as parallel column +//! vectors, deduplicated by `measurement_id`, then flushed to DuckDB +//! via `Appender::append_record_batch` as one Arrow `RecordBatch` per +//! fact table. + +use std::collections::BTreeMap; +use std::io::BufRead; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; + +use anyhow::Context as _; +use anyhow::Result; +use arrow_array::ArrayRef; +use arrow_array::Int32Array; +use arrow_array::Int64Array; +use arrow_array::ListArray; +use arrow_array::RecordBatch; +use arrow_array::StringArray; +use arrow_buffer::OffsetBuffer; +use arrow_schema::DataType; +use arrow_schema::Field; +use arrow_schema::Schema; +use duckdb::Connection; +use tracing::info; +use tracing::warn; +use vortex_bench_server::db::measurement_id_compression_size; +use vortex_bench_server::db::measurement_id_compression_time; +use vortex_bench_server::db::measurement_id_query; +use vortex_bench_server::db::measurement_id_random_access; +use vortex_bench_server::records::CompressionSize; +use vortex_bench_server::records::CompressionTime; +use vortex_bench_server::records::QueryMeasurement; +use vortex_bench_server::records::RandomAccessTime; +use vortex_bench_server::schema::SCHEMA_DDL; +use vortex_utils::aliases::hash_map::HashMap; + +use crate::classifier; +use crate::classifier::V3Bin; +use crate::commits::upsert_commit; +use crate::source::Source; +use crate::v2::V2Commit; +use crate::v2::V2FileSize; +use crate::v2::V2Record; +use crate::v2::index_commits; +use crate::v2::runtime_as_i64; +use crate::v2::value_as_f64; + +/// Per-table insert counts, plus skip / missing counts. +#[derive(Debug, Default, Clone)] +pub struct MigrationSummary { + pub records_read: u64, + pub query_inserted: u64, + pub compression_time_inserted: u64, + pub compression_size_inserted: u64, + pub random_access_inserted: u64, + pub file_size_inserted: u64, + pub uncategorized: u64, + pub uncategorized_prefixes: BTreeMap, + pub missing_commit: u64, + pub commit_warnings: u64, + pub skipped_no_value: u64, + pub skipped_intentional: u64, + pub commits_inserted: u64, + pub deduped: u64, + /// Number of records dropped by dedup whose `value_ns` (or + /// `value_bytes` for compression_sizes' replace path) differed + /// from the kept row's. Non-zero is a smell worth investigating. + pub deduped_with_conflict: u64, +} + +impl MigrationSummary { + /// Total `data.json.gz` records that landed in some v3 fact table. + pub fn total_inserted(&self) -> u64 { + self.query_inserted + + self.compression_time_inserted + + self.compression_size_inserted + + self.random_access_inserted + } + + /// Fraction of records that were uncategorized. The orchestrator + /// stops if this exceeds the documented 5% threshold. + pub fn uncategorized_fraction(&self) -> f64 { + if self.records_read == 0 { + return 0.0; + } + self.uncategorized as f64 / self.records_read as f64 + } +} + +/// Open or create a DuckDB at `path` and apply the v3 schema. The +/// migrator is a one-shot fresh load; the bulk-append flush is pure +/// insert (no `ON CONFLICT`), so any stale rows in `path` would clash +/// with the next run on the same primary keys. Delete both the +/// database file and its WAL companion up front so every run starts +/// from a known-empty state. +pub fn open_target_db(path: &Path) -> Result { + remove_if_exists(path)?; + let wal = wal_path(path); + remove_if_exists(&wal)?; + let conn = + Connection::open(path).with_context(|| format!("opening DuckDB at {}", path.display()))?; + conn.execute_batch(SCHEMA_DDL) + .context("applying v3 schema DDL")?; + Ok(conn) +} + +fn remove_if_exists(path: &Path) -> Result<()> { + match std::fs::remove_file(path) { + Ok(()) => { + info!(path = %path.display(), "removed pre-existing target file"); + Ok(()) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(e).with_context(|| format!("removing {}", path.display())), + } +} + +/// DuckDB writes its write-ahead log next to the database file with a +/// `.wal` suffix appended (e.g. `v3.duckdb` -> `v3.duckdb.wal`). +fn wal_path(path: &Path) -> std::path::PathBuf { + let mut name = path.as_os_str().to_owned(); + name.push(".wal"); + std::path::PathBuf::from(name) +} + +/// Run the whole migration: commits, data.json.gz, and every +/// file-sizes-*.json.gz under the source. +pub fn run(source: &Source, target: &Path) -> Result { + let mut conn = open_target_db(target)?; + let mut summary = MigrationSummary::default(); + + info!(source = %source.describe(), "Reading commits.json"); + let commits = read_commits(source)?; + info!(commits = commits.len(), "Loaded commits"); + summary.commits_inserted = upsert_all_commits(&mut conn, &commits, &mut summary)?; + + let mut q = QueryAccum::default(); + let mut ct = CompressionTimeAccum::default(); + let mut cs = CompressionSizeAccum::default(); + let mut ra = RandomAccessAccum::default(); + + info!("Migrating data.json.gz"); + migrate_data_jsonl( + source, + &commits, + &mut summary, + &mut q, + &mut ct, + &mut cs, + &mut ra, + )?; + info!(records = summary.records_read, "data.json.gz done"); + + for name in source.list_file_sizes()? { + info!(name = %name, "Migrating file-sizes"); + if let Err(e) = migrate_file_sizes(source, &name, &commits, &mut summary, &mut cs) { + warn!("file-sizes file {name} failed: {e:#}"); + } + } + + info!("Flushing accumulators to DuckDB"); + summary.query_inserted = q.measurement_id.len() as u64; + summary.compression_time_inserted = ct.measurement_id.len() as u64; + summary.random_access_inserted = ra.measurement_id.len() as u64; + summary.compression_size_inserted = cs.rows.len() as u64; + + flush(&conn, "query_measurements", build_query_batch(q)?)?; + flush( + &conn, + "compression_times", + build_compression_time_batch(ct)?, + )?; + flush(&conn, "random_access_times", build_random_access_batch(ra)?)?; + flush( + &conn, + "compression_sizes", + build_compression_size_batch(cs)?, + )?; + + Ok(summary) +} + +fn read_commits(source: &Source) -> Result> { + let reader = source.open_commits_jsonl()?; + let mut commits: Vec = Vec::new(); + for line in reader.lines() { + let line = line?; + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + match serde_json::from_str::(trimmed) { + Ok(c) => commits.push(c), + Err(e) => warn!("skipping malformed commits.json line: {e}"), + } + } + Ok(index_commits(commits)) +} + +fn upsert_all_commits( + conn: &mut Connection, + commits: &BTreeMap, + summary: &mut MigrationSummary, +) -> Result { + let tx = conn.transaction().context("begin commits transaction")?; + let mut count = 0u64; + for commit in commits.values() { + let outcome = upsert_commit(&tx, commit)?; + for w in outcome.warnings { + warn!("{w}"); + summary.commit_warnings += 1; + } + count += 1; + } + tx.commit().context("commit commits transaction")?; + Ok(count) +} + +/// Stream `data.json.gz` and push classified records into the +/// per-table accumulators. Dedup happens inside each accumulator's +/// `push` method by `measurement_id`. +fn migrate_data_jsonl( + source: &Source, + commits: &BTreeMap, + summary: &mut MigrationSummary, + q: &mut QueryAccum, + ct: &mut CompressionTimeAccum, + cs: &mut CompressionSizeAccum, + ra: &mut RandomAccessAccum, +) -> Result<()> { + let reader = source.open_data_jsonl()?; + let started = Instant::now(); + let mut last_log = Instant::now(); + for line in reader.lines() { + let line = line?; + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + summary.records_read += 1; + let record: V2Record = match serde_json::from_str(trimmed) { + Ok(r) => r, + Err(e) => { + warn!("skipping malformed data.json line: {e}"); + continue; + } + }; + apply_v2_record(&record, commits, summary, q, ct, cs, ra); + if last_log.elapsed() >= Duration::from_secs(5) { + let elapsed = started.elapsed().as_secs_f64(); + let rate = summary.records_read as f64 / elapsed.max(0.001); + info!( + records = summary.records_read, + rate = format!("{rate:.0}/s"), + query = q.measurement_id.len(), + compression_time = ct.measurement_id.len(), + compression_size = cs.rows.len(), + random_access = ra.measurement_id.len(), + "migration progress", + ); + last_log = Instant::now(); + } + } + Ok(()) +} + +fn apply_v2_record( + record: &V2Record, + commits: &BTreeMap, + summary: &mut MigrationSummary, + q: &mut QueryAccum, + ct: &mut CompressionTimeAccum, + cs: &mut CompressionSizeAccum, + ra: &mut RandomAccessAccum, +) { + let Some(sha) = record.commit_id.clone() else { + summary.missing_commit += 1; + return; + }; + if !commits.contains_key(&sha) { + summary.missing_commit += 1; + return; + } + + let bin = match classifier::classify_outcome(record) { + classifier::Outcome::Bin(b) => b, + classifier::Outcome::Skip(_) => { + summary.skipped_intentional += 1; + return; + } + classifier::Outcome::Unknown => { + summary.uncategorized += 1; + let prefix = record.name.split('/').next().unwrap_or("").to_string(); + *summary.uncategorized_prefixes.entry(prefix).or_insert(0) += 1; + return; + } + }; + + let env_triple = record.env_triple.as_ref().and_then(|t| t.to_triple()); + let runtimes = record + .all_runtimes + .as_ref() + .map(|v| v.iter().filter_map(runtime_as_i64).collect::>()) + .unwrap_or_default(); + let value_f64 = match record.value.as_ref().and_then(value_as_f64) { + Some(v) => v, + None => { + summary.skipped_no_value += 1; + return; + } + }; + + match bin { + V3Bin::Query { + dataset, + dataset_variant, + scale_factor, + query_idx, + storage, + engine, + format, + } => { + let qm = QueryMeasurement { + commit_sha: sha, + dataset, + dataset_variant, + scale_factor, + query_idx, + storage, + engine, + format, + value_ns: value_f64 as i64, + all_runtimes_ns: runtimes, + peak_physical: None, + peak_virtual: None, + physical_delta: None, + virtual_delta: None, + env_triple, + }; + let mid = measurement_id_query(&qm); + q.push(mid, qm, summary); + } + V3Bin::CompressionTime { + dataset, + dataset_variant, + format, + op, + } => { + let ctr = CompressionTime { + commit_sha: sha, + dataset, + dataset_variant, + format, + op, + value_ns: value_f64 as i64, + all_runtimes_ns: runtimes, + env_triple, + }; + let mid = measurement_id_compression_time(&ctr); + ct.push(mid, ctr, summary); + } + V3Bin::CompressionSize { + dataset, + dataset_variant, + format, + } => { + let csr = CompressionSize { + commit_sha: sha, + dataset, + dataset_variant, + format, + value_bytes: value_f64 as i64, + }; + let mid = measurement_id_compression_size(&csr); + cs.push_replace(mid, csr, summary); + } + V3Bin::RandomAccess { dataset, format } => { + let rar = RandomAccessTime { + commit_sha: sha, + dataset, + format, + value_ns: value_f64 as i64, + all_runtimes_ns: runtimes, + env_triple, + }; + let mid = measurement_id_random_access(&rar); + ra.push(mid, rar, summary); + } + } +} + +fn migrate_file_sizes( + source: &Source, + name: &str, + commits: &BTreeMap, + summary: &mut MigrationSummary, + cs: &mut CompressionSizeAccum, +) -> Result<()> { + let reader = source.open_file_sizes(name)?; + let dataset_fallback = name + .strip_prefix("file-sizes-") + .and_then(|s| s.strip_suffix(".json.gz")) + .unwrap_or(name) + .to_string(); + let started = Instant::now(); + let mut last_log = Instant::now(); + for line in reader.lines() { + let line = line?; + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let sz: V2FileSize = match serde_json::from_str(trimmed) { + Ok(r) => r, + Err(e) => { + warn!("skipping malformed {name} line: {e}"); + continue; + } + }; + if !commits.contains_key(&sz.commit_id) { + summary.missing_commit += 1; + continue; + } + let dataset = if sz.benchmark.is_empty() { + dataset_fallback.clone() + } else { + sz.benchmark.clone() + }; + let dataset_variant = sz + .scale_factor + .as_ref() + .filter(|s| !s.is_empty() && s.as_str() != "1.0") + .cloned(); + let csr = CompressionSize { + commit_sha: sz.commit_id.clone(), + dataset, + dataset_variant, + format: sz.format.clone(), + value_bytes: sz.size_bytes, + }; + let mid = measurement_id_compression_size(&csr); + cs.push_sum(mid, csr); + summary.file_size_inserted += 1; + if last_log.elapsed() >= Duration::from_secs(5) { + let elapsed = started.elapsed().as_secs_f64(); + let rate = summary.file_size_inserted as f64 / elapsed.max(0.001); + info!( + name = %name, + file_sizes = summary.file_size_inserted, + rate = format!("{rate:.0}/s"), + "file-sizes progress", + ); + last_log = Instant::now(); + } + } + Ok(()) +} + +/// Append an Arrow `RecordBatch` to a DuckDB table via `Appender`. +fn flush(conn: &Connection, table: &str, batch: RecordBatch) -> Result<()> { + let mut app = conn + .appender(table) + .with_context(|| format!("opening appender for {table}"))?; + app.append_record_batch(batch) + .with_context(|| format!("appending record batch to {table}"))?; + drop(app); + Ok(()) +} + +#[derive(Default)] +struct QueryAccum { + measurement_id: Vec, + commit_sha: Vec, + dataset: Vec, + dataset_variant: Vec>, + scale_factor: Vec>, + query_idx: Vec, + storage: Vec, + engine: Vec, + format: Vec, + value_ns: Vec, + all_runtimes_ns: Vec>, + peak_physical: Vec>, + peak_virtual: Vec>, + physical_delta: Vec>, + virtual_delta: Vec>, + env_triple: Vec>, + /// `mid` -> index in the parallel column vecs. Lets us look up the + /// kept row's `value_ns` on collision so we can flag conflicts. + seen: HashMap, +} + +impl QueryAccum { + fn push(&mut self, mid: i64, r: QueryMeasurement, summary: &mut MigrationSummary) { + if let Some(&idx) = self.seen.get(&mid) { + summary.deduped += 1; + if self.value_ns[idx] != r.value_ns { + summary.deduped_with_conflict += 1; + } + return; + } + let idx = self.measurement_id.len(); + self.seen.insert(mid, idx); + self.measurement_id.push(mid); + self.commit_sha.push(r.commit_sha); + self.dataset.push(r.dataset); + self.dataset_variant.push(r.dataset_variant); + self.scale_factor.push(r.scale_factor); + self.query_idx.push(r.query_idx); + self.storage.push(r.storage); + self.engine.push(r.engine); + self.format.push(r.format); + self.value_ns.push(r.value_ns); + self.all_runtimes_ns.push(r.all_runtimes_ns); + self.peak_physical.push(r.peak_physical); + self.peak_virtual.push(r.peak_virtual); + self.physical_delta.push(r.physical_delta); + self.virtual_delta.push(r.virtual_delta); + self.env_triple.push(r.env_triple); + } +} + +#[derive(Default)] +struct CompressionTimeAccum { + measurement_id: Vec, + commit_sha: Vec, + dataset: Vec, + dataset_variant: Vec>, + format: Vec, + op: Vec, + value_ns: Vec, + all_runtimes_ns: Vec>, + env_triple: Vec>, + seen: HashMap, +} + +impl CompressionTimeAccum { + fn push(&mut self, mid: i64, r: CompressionTime, summary: &mut MigrationSummary) { + if let Some(&idx) = self.seen.get(&mid) { + summary.deduped += 1; + if self.value_ns[idx] != r.value_ns { + summary.deduped_with_conflict += 1; + } + return; + } + let idx = self.measurement_id.len(); + self.seen.insert(mid, idx); + self.measurement_id.push(mid); + self.commit_sha.push(r.commit_sha); + self.dataset.push(r.dataset); + self.dataset_variant.push(r.dataset_variant); + self.format.push(r.format); + self.op.push(r.op); + self.value_ns.push(r.value_ns); + self.all_runtimes_ns.push(r.all_runtimes_ns); + self.env_triple.push(r.env_triple); + } +} + +#[derive(Default)] +struct RandomAccessAccum { + measurement_id: Vec, + commit_sha: Vec, + dataset: Vec, + format: Vec, + value_ns: Vec, + all_runtimes_ns: Vec>, + env_triple: Vec>, + seen: HashMap, +} + +impl RandomAccessAccum { + fn push(&mut self, mid: i64, r: RandomAccessTime, summary: &mut MigrationSummary) { + if let Some(&idx) = self.seen.get(&mid) { + summary.deduped += 1; + if self.value_ns[idx] != r.value_ns { + summary.deduped_with_conflict += 1; + } + return; + } + let idx = self.measurement_id.len(); + self.seen.insert(mid, idx); + self.measurement_id.push(mid); + self.commit_sha.push(r.commit_sha); + self.dataset.push(r.dataset); + self.format.push(r.format); + self.value_ns.push(r.value_ns); + self.all_runtimes_ns.push(r.all_runtimes_ns); + self.env_triple.push(r.env_triple); + } +} + +/// `compression_sizes` is fed by both data.json.gz (replace-on-collision) +/// and file-sizes-*.json.gz (sum-on-collision). Stored as a map; converted +/// to a `RecordBatch` at flush time. +#[derive(Default)] +struct CompressionSizeAccum { + rows: HashMap, +} + +impl CompressionSizeAccum { + /// data.json.gz path: latest write wins, mirroring the prior + /// `ON CONFLICT DO UPDATE SET value_bytes = excluded.value_bytes`. + /// Bumps `deduped_with_conflict` when an existing row's + /// `value_bytes` differs from the incoming row's, so silent + /// value-corruption is observable. + fn push_replace(&mut self, mid: i64, r: CompressionSize, summary: &mut MigrationSummary) { + if let Some(existing) = self.rows.get(&mid) + && existing.value_bytes != r.value_bytes + { + summary.deduped_with_conflict += 1; + } + self.rows.insert(mid, r); + } + + /// file-sizes-*.json.gz path: per-file rows aggregate into one + /// `(commit, dataset, dataset_variant, format)` row by summing, + /// mirroring the prior `value_bytes = compression_sizes.value_bytes + /// + excluded.value_bytes`. + fn push_sum(&mut self, mid: i64, r: CompressionSize) { + let add = r.value_bytes; + self.rows + .entry(mid) + .and_modify(|x| x.value_bytes += add) + .or_insert(r); + } +} + +fn build_query_batch(a: QueryAccum) -> Result { + let schema = Arc::new(Schema::new(vec![ + Field::new("measurement_id", DataType::Int64, false), + Field::new("commit_sha", DataType::Utf8, false), + Field::new("dataset", DataType::Utf8, false), + Field::new("dataset_variant", DataType::Utf8, true), + Field::new("scale_factor", DataType::Utf8, true), + Field::new("query_idx", DataType::Int32, false), + Field::new("storage", DataType::Utf8, false), + Field::new("engine", DataType::Utf8, false), + Field::new("format", DataType::Utf8, false), + Field::new("value_ns", DataType::Int64, false), + Field::new( + "all_runtimes_ns", + DataType::List(Arc::new(Field::new("item", DataType::Int64, false))), + false, + ), + Field::new("peak_physical", DataType::Int64, true), + Field::new("peak_virtual", DataType::Int64, true), + Field::new("physical_delta", DataType::Int64, true), + Field::new("virtual_delta", DataType::Int64, true), + Field::new("env_triple", DataType::Utf8, true), + ])); + let cols: Vec = vec![ + Arc::new(Int64Array::from(a.measurement_id)), + Arc::new(StringArray::from(a.commit_sha)), + Arc::new(StringArray::from(a.dataset)), + Arc::new(StringArray::from(a.dataset_variant)), + Arc::new(StringArray::from(a.scale_factor)), + Arc::new(Int32Array::from(a.query_idx)), + Arc::new(StringArray::from(a.storage)), + Arc::new(StringArray::from(a.engine)), + Arc::new(StringArray::from(a.format)), + Arc::new(Int64Array::from(a.value_ns)), + Arc::new(build_list_int64(a.all_runtimes_ns)), + Arc::new(Int64Array::from(a.peak_physical)), + Arc::new(Int64Array::from(a.peak_virtual)), + Arc::new(Int64Array::from(a.physical_delta)), + Arc::new(Int64Array::from(a.virtual_delta)), + Arc::new(StringArray::from(a.env_triple)), + ]; + Ok(RecordBatch::try_new(schema, cols)?) +} + +fn build_compression_time_batch(a: CompressionTimeAccum) -> Result { + let schema = Arc::new(Schema::new(vec![ + Field::new("measurement_id", DataType::Int64, false), + Field::new("commit_sha", DataType::Utf8, false), + Field::new("dataset", DataType::Utf8, false), + Field::new("dataset_variant", DataType::Utf8, true), + Field::new("format", DataType::Utf8, false), + Field::new("op", DataType::Utf8, false), + Field::new("value_ns", DataType::Int64, false), + Field::new( + "all_runtimes_ns", + DataType::List(Arc::new(Field::new("item", DataType::Int64, false))), + false, + ), + Field::new("env_triple", DataType::Utf8, true), + ])); + let cols: Vec = vec![ + Arc::new(Int64Array::from(a.measurement_id)), + Arc::new(StringArray::from(a.commit_sha)), + Arc::new(StringArray::from(a.dataset)), + Arc::new(StringArray::from(a.dataset_variant)), + Arc::new(StringArray::from(a.format)), + Arc::new(StringArray::from(a.op)), + Arc::new(Int64Array::from(a.value_ns)), + Arc::new(build_list_int64(a.all_runtimes_ns)), + Arc::new(StringArray::from(a.env_triple)), + ]; + Ok(RecordBatch::try_new(schema, cols)?) +} + +fn build_random_access_batch(a: RandomAccessAccum) -> Result { + let schema = Arc::new(Schema::new(vec![ + Field::new("measurement_id", DataType::Int64, false), + Field::new("commit_sha", DataType::Utf8, false), + Field::new("dataset", DataType::Utf8, false), + Field::new("format", DataType::Utf8, false), + Field::new("value_ns", DataType::Int64, false), + Field::new( + "all_runtimes_ns", + DataType::List(Arc::new(Field::new("item", DataType::Int64, false))), + false, + ), + Field::new("env_triple", DataType::Utf8, true), + ])); + let cols: Vec = vec![ + Arc::new(Int64Array::from(a.measurement_id)), + Arc::new(StringArray::from(a.commit_sha)), + Arc::new(StringArray::from(a.dataset)), + Arc::new(StringArray::from(a.format)), + Arc::new(Int64Array::from(a.value_ns)), + Arc::new(build_list_int64(a.all_runtimes_ns)), + Arc::new(StringArray::from(a.env_triple)), + ]; + Ok(RecordBatch::try_new(schema, cols)?) +} + +fn build_compression_size_batch(a: CompressionSizeAccum) -> Result { + let n = a.rows.len(); + let mut measurement_id = Vec::with_capacity(n); + let mut commit_sha = Vec::with_capacity(n); + let mut dataset = Vec::with_capacity(n); + let mut dataset_variant = Vec::with_capacity(n); + let mut format = Vec::with_capacity(n); + let mut value_bytes = Vec::with_capacity(n); + for (mid, cs) in a.rows { + measurement_id.push(mid); + commit_sha.push(cs.commit_sha); + dataset.push(cs.dataset); + dataset_variant.push(cs.dataset_variant); + format.push(cs.format); + value_bytes.push(cs.value_bytes); + } + let schema = Arc::new(Schema::new(vec![ + Field::new("measurement_id", DataType::Int64, false), + Field::new("commit_sha", DataType::Utf8, false), + Field::new("dataset", DataType::Utf8, false), + Field::new("dataset_variant", DataType::Utf8, true), + Field::new("format", DataType::Utf8, false), + Field::new("value_bytes", DataType::Int64, false), + ])); + let cols: Vec = vec![ + Arc::new(Int64Array::from(measurement_id)), + Arc::new(StringArray::from(commit_sha)), + Arc::new(StringArray::from(dataset)), + Arc::new(StringArray::from(dataset_variant)), + Arc::new(StringArray::from(format)), + Arc::new(Int64Array::from(value_bytes)), + ]; + Ok(RecordBatch::try_new(schema, cols)?) +} + +/// Build a non-nullable `List` Arrow array from one inner Vec +/// per row. The outer list is non-null; inner i64 values are non-null. +fn build_list_int64(values: Vec>) -> ListArray { + let mut offsets: Vec = Vec::with_capacity(values.len() + 1); + offsets.push(0); + let mut flat: Vec = Vec::new(); + for inner in values { + flat.extend_from_slice(&inner); + offsets.push(flat.len() as i32); + } + let values_arr = Int64Array::from(flat); + let field = Arc::new(Field::new("item", DataType::Int64, false)); + ListArray::new( + field, + OffsetBuffer::new(offsets.into()), + Arc::new(values_arr), + None, + ) +} + +/// Print the summary in a human-readable form. Returned by the CLI. +impl std::fmt::Display for MigrationSummary { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Records read: {}", self.records_read)?; + writeln!(f, "Commits upserted: {}", self.commits_inserted)?; + writeln!(f, "Commit warnings: {}", self.commit_warnings)?; + writeln!(f, "Inserted (query): {}", self.query_inserted)?; + writeln!( + f, + "Inserted (compress t): {}", + self.compression_time_inserted + )?; + writeln!( + f, + "Inserted (compress s): {}", + self.compression_size_inserted + )?; + writeln!(f, "Inserted (random acc): {}", self.random_access_inserted)?; + writeln!(f, "Inserted (file sizes): {}", self.file_size_inserted)?; + writeln!(f, "Missing commit: {}", self.missing_commit)?; + writeln!(f, "Skipped (no value): {}", self.skipped_no_value)?; + writeln!(f, "Skipped (intentional): {}", self.skipped_intentional)?; + writeln!(f, "Deduplicated: {}", self.deduped)?; + writeln!(f, "Dedup w/ value diff: {}", self.deduped_with_conflict)?; + writeln!( + f, + "Uncategorized: {} ({:.2}%)", + self.uncategorized, + 100.0 * self.uncategorized_fraction() + )?; + if !self.uncategorized_prefixes.is_empty() { + let mut top: Vec<_> = self.uncategorized_prefixes.iter().collect(); + top.sort_by(|a, b| b.1.cmp(a.1)); + writeln!(f, "Top uncategorized prefixes:")?; + for (prefix, n) in top.iter().take(20) { + writeln!(f, " {prefix:>32} : {n}")?; + } + } + Ok(()) + } +} diff --git a/benchmarks-website/migrate/src/source.rs b/benchmarks-website/migrate/src/source.rs new file mode 100644 index 00000000000..c18e86a63ca --- /dev/null +++ b/benchmarks-website/migrate/src/source.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Streaming readers for v2's public S3 bucket. +//! +//! The bucket is `--no-sign-request`, so we fetch the underlying +//! HTTPS URL directly and stream-decompress with `flate2`. The +//! downloads are wrapped in [`reqwest::blocking`] to keep the read +//! path synchronous; the binary's hot path is single-threaded +//! per-source already (DuckDB is a single-writer). +//! +//! For tests and offline runs, [`Source::Local`] accepts a local +//! directory of dumps; the migrator's `--source` flag picks the +//! variant. + +use std::fs::File; +use std::io::BufRead; +use std::io::BufReader; +use std::io::Read; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::Context as _; +use anyhow::Result; +use flate2::read::GzDecoder; +use tracing::info; + +/// Public S3 bucket the live v2 server reads from. +pub const PUBLIC_BUCKET_BASE: &str = "https://vortex-ci-benchmark-results.s3.amazonaws.com"; + +/// Where to read the v2 dataset from. Either the public S3 bucket +/// (the live deployment) or a local directory of dumps. +#[derive(Debug, Clone)] +pub enum Source { + /// HTTPS GETs against `s3.amazonaws.com`. + PublicS3, + /// A directory containing `data.json.gz`, `commits.json`, and + /// `file-sizes-*.json.gz` files. + Local(PathBuf), +} + +impl Source { + /// Short human-readable description for log messages. + pub fn describe(&self) -> String { + match self { + Source::PublicS3 => "public S3 bucket".to_string(), + Source::Local(p) => format!("local dir {}", p.display()), + } + } + + /// Open `data.json.gz` for streaming, decompressing on the fly. + pub fn open_data_jsonl(&self) -> Result> { + let stream = self.open_raw("data.json.gz")?; + Ok(Box::new(BufReader::new(GzDecoder::new(stream)))) + } + + /// Open `commits.json` (uncompressed). + pub fn open_commits_jsonl(&self) -> Result> { + let stream = self.open_raw("commits.json")?; + Ok(Box::new(BufReader::new(stream))) + } + + /// Enumerate `file-sizes-*.json.gz` files. For local sources this + /// is a directory glob; for the public bucket we hit the documented + /// suite ids. + pub fn list_file_sizes(&self) -> Result> { + match self { + Source::Local(dir) => { + let mut out = Vec::new(); + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let name = entry.file_name(); + let s = name.to_string_lossy(); + if s.starts_with("file-sizes-") && s.ends_with(".json.gz") { + out.push(s.into_owned()); + } + } + out.sort(); + Ok(out) + } + Source::PublicS3 => { + // The S3 bucket's ListObjects is denied for unsigned + // requests, so we hit the documented per-suite keys + // emitted by `.github/workflows/sql-benchmarks.yml`. + Ok(KNOWN_FILE_SIZES_SUITES + .iter() + .map(|id| format!("file-sizes-{id}.json.gz")) + .collect()) + } + } + } + + /// Open one `file-sizes-*.json.gz` for streaming. + pub fn open_file_sizes(&self, name: &str) -> Result> { + let stream = self.open_raw(name)?; + Ok(Box::new(BufReader::new(GzDecoder::new(stream)))) + } + + fn open_raw(&self, name: &str) -> Result> { + match self { + Source::Local(dir) => open_local(&dir.join(name)), + Source::PublicS3 => open_s3(name), + } + } +} + +fn open_local(path: &Path) -> Result> { + let f = File::open(path).with_context(|| format!("opening {}", path.display()))?; + Ok(Box::new(f)) +} + +fn open_s3(name: &str) -> Result> { + let url = format!("{PUBLIC_BUCKET_BASE}/{name}"); + info!(url = %url, "GET"); + let resp = reqwest::blocking::get(&url).with_context(|| format!("GET {url}"))?; + if !resp.status().is_success() { + anyhow::bail!("GET {url} returned {}", resp.status()); + } + Ok(Box::new(resp)) +} + +/// Suite IDs we know publish a `file-sizes-{id}.json.gz` to S3. +/// +/// Source of truth: the `matrix.id` values in +/// `.github/workflows/sql-benchmarks.yml`'s `benchmark_matrix` default. +/// The post-bench `file-sizes` step uploads `file-sizes-${{ matrix.id +/// }}.json.gz`, so this list must match those IDs verbatim. Adding a +/// new matrix entry to that workflow means adding the same ID here. +const KNOWN_FILE_SIZES_SUITES: &[&str] = &[ + "clickbench-nvme", + "tpch-nvme", + "tpch-s3", + "tpch-nvme-10", + "tpch-s3-10", + "tpcds-nvme", + "statpopgen", + "fineweb", + "fineweb-s3", + "polarsignals", +]; diff --git a/benchmarks-website/migrate/src/v2.rs b/benchmarks-website/migrate/src/v2.rs new file mode 100644 index 00000000000..2a9d3bdf5d0 --- /dev/null +++ b/benchmarks-website/migrate/src/v2.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Wire shapes of the v2 benchmark dataset on S3. +//! +//! These types capture only the fields the migrator reads. v2 records +//! are serialized by `vortex-bench` (see `vortex-bench/src/measurements.rs`) +//! and by older non-Rust scripts; the union of fields is loose, so we +//! deserialize permissively (`serde(default)`, untyped `serde_json::Value` +//! for the polymorphic `dataset` field). + +use std::collections::BTreeMap; + +use serde::Deserialize; + +/// One JSONL line of `data.json.gz`. +/// +/// The shape is the union of every emitter's output. Most fields are +/// optional because different benches emit different subsets. +#[derive(Debug, Clone, Deserialize)] +pub struct V2Record { + pub name: String, + #[serde(default)] + pub commit_id: Option, + #[serde(default)] + pub unit: Option, + #[serde(default)] + pub value: Option, + #[serde(default)] + pub storage: Option, + #[serde(default)] + pub dataset: Option, + #[serde(default)] + pub all_runtimes: Option>, + #[serde(default)] + pub env_triple: Option, +} + +/// `dataset` in v2 records is sometimes a string, sometimes an object +/// keyed by suite name (`{ "tpch": { "scale_factor": "10" } }`). +/// This helper looks up the scale factor for a given suite without +/// assuming a particular shape. +pub fn dataset_scale_factor(dataset: &serde_json::Value, key: &str) -> Option { + let obj = dataset.as_object()?; + let entry = obj.get(key)?; + let sf = entry.get("scale_factor")?; + match sf { + serde_json::Value::String(s) => Some(s.clone()), + serde_json::Value::Number(n) => Some(n.to_string()), + _ => None, + } +} + +/// Best-effort numeric coercion for the polymorphic `value` field. +pub fn value_as_f64(value: &serde_json::Value) -> Option { + match value { + serde_json::Value::Number(n) => n.as_f64(), + serde_json::Value::String(s) => s.parse().ok(), + _ => None, + } +} + +/// Best-effort coercion of a runtime entry to nanoseconds. +pub fn runtime_as_i64(value: &serde_json::Value) -> Option { + match value { + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Some(i) + } else { + n.as_f64().map(|f| f as i64) + } + } + serde_json::Value::String(s) => s.parse().ok(), + _ => None, + } +} + +/// Triple block as emitted by `vortex-bench`'s `--gh-json` path. v2 +/// stored it as an object; we serialize it back out as `arch-os-env`. +#[derive(Debug, Clone, Deserialize)] +pub struct V2EnvTriple { + #[serde(default)] + pub architecture: Option, + #[serde(default)] + pub operating_system: Option, + #[serde(default)] + pub environment: Option, +} + +impl V2EnvTriple { + /// Format as the `arch-os-env` triple used by v3's `env_triple` column. + pub fn to_triple(&self) -> Option { + let arch = self.architecture.as_deref()?; + let os = self.operating_system.as_deref()?; + let env = self.environment.as_deref()?; + Some(format!("{arch}-{os}-{env}")) + } +} + +/// One JSONL line of `commits.json`. +#[derive(Debug, Clone, Deserialize)] +pub struct V2Commit { + pub id: String, + #[serde(default)] + pub timestamp: Option, + #[serde(default)] + pub message: Option, + #[serde(default)] + pub author: Option, + #[serde(default)] + pub committer: Option, + #[serde(default)] + pub tree_id: Option, + #[serde(default)] + pub url: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct V2Person { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub email: Option, +} + +/// One JSONL line of `file-sizes-*.json.gz` produced by +/// `scripts/capture-file-sizes.py`. +#[derive(Debug, Clone, Deserialize)] +pub struct V2FileSize { + pub commit_id: String, + pub benchmark: String, + #[serde(default)] + pub scale_factor: Option, + pub format: String, + pub file: String, + pub size_bytes: i64, +} + +/// Build a sha-keyed map of commits. +pub fn index_commits(commits: Vec) -> BTreeMap { + commits.into_iter().map(|c| (c.id.clone(), c)).collect() +} diff --git a/benchmarks-website/migrate/src/verify.rs b/benchmarks-website/migrate/src/verify.rs new file mode 100644 index 00000000000..eb4caef6df7 --- /dev/null +++ b/benchmarks-website/migrate/src/verify.rs @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Structural diff between a migrated v3 DuckDB and the live v2 +//! `/api/metadata` endpoint. +//! +//! Compares group / chart structure only; values aren't compared +//! because v2 converts ns → ms and bytes → MiB on read while v3 +//! stores raw and the chart query divides. Group/chart structural +//! equivalence is enough to spot classifier regressions before +//! cutover. + +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::path::Path; + +use anyhow::Context as _; +use anyhow::Result; +use duckdb::Connection; +use serde::Deserialize; + +use crate::classifier::QUERY_SUITES; + +/// Result of one `verify` run. +#[derive(Debug, Default)] +pub struct VerifyReport { + pub matched_groups: Vec, + pub only_in_v3: Vec, + pub only_in_v2: Vec, + pub chart_diffs: Vec, +} + +#[derive(Debug, Clone)] +pub struct ChartDiff { + pub group: String, + pub v2_count: usize, + pub v3_count: usize, +} + +impl VerifyReport { + /// True if every v2 group is represented in v3. The CLI's exit + /// code reflects this. + pub fn v2_groups_covered(&self) -> bool { + self.only_in_v2.is_empty() + } +} + +impl std::fmt::Display for VerifyReport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Groups in both v2 and v3:")?; + for g in &self.matched_groups { + writeln!(f, " + {g}")?; + } + if !self.only_in_v2.is_empty() { + writeln!(f, "Groups only in v2 (regression candidates):")?; + for g in &self.only_in_v2 { + writeln!(f, " - {g}")?; + } + } + if !self.only_in_v3.is_empty() { + writeln!(f, "Groups only in v3:")?; + for g in &self.only_in_v3 { + writeln!(f, " + {g}")?; + } + } + if !self.chart_diffs.is_empty() { + writeln!(f, "Chart count diffs:")?; + for d in &self.chart_diffs { + writeln!( + f, + " {} : v2={} v3={} (delta={})", + d.group, + d.v2_count, + d.v3_count, + d.v3_count as i64 - d.v2_count as i64, + )?; + } + } + Ok(()) + } +} + +/// v2's `/api/metadata` reply — only the fields we need. +#[derive(Debug, Deserialize)] +struct V2Metadata { + groups: BTreeMap, +} + +#[derive(Debug, Deserialize)] +struct V2GroupMeta { + #[serde(default)] + charts: Vec, +} + +#[derive(Debug, Deserialize)] +struct V2ChartMeta { + #[serde(default)] + name: String, +} + +/// Open the migrated DuckDB at `duckdb_path`, fetch `/api/metadata`, +/// and produce a structural diff. +pub fn run(v2_server: &str, duckdb_path: &Path) -> Result { + let v3 = collect_v3_groups(duckdb_path)?; + let v2 = fetch_v2_metadata(v2_server)?; + Ok(diff(&v2, &v3)) +} + +fn collect_v3_groups(duckdb_path: &Path) -> Result>> { + let conn = Connection::open(duckdb_path) + .with_context(|| format!("opening DuckDB at {}", duckdb_path.display()))?; + let mut groups: BTreeMap> = BTreeMap::new(); + + // query_measurements: chart per (dataset, query_idx); group per + // (dataset, dataset_variant, scale_factor, storage). We want v2 + // group display names so the verifier can compare apples to + // apples, so we re-format them here using the same suite table. + let mut stmt = conn.prepare( + r#" + SELECT dataset, dataset_variant, scale_factor, storage, query_idx + FROM query_measurements + GROUP BY dataset, dataset_variant, scale_factor, storage, query_idx + "#, + )?; + let rows = stmt.query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, Option>(1)?, + row.get::<_, Option>(2)?, + row.get::<_, String>(3)?, + row.get::<_, i32>(4)?, + )) + })?; + for row in rows { + let (dataset, _variant, sf, storage, query_idx) = row?; + let group_name = display_query_group(&dataset, sf.as_deref(), &storage); + let chart_name = chart_name_query(&dataset, query_idx); + groups + .entry(group_name) + .or_default() + .insert(normalize_chart(&chart_name)); + } + + // compression_times: group "Compression", charts per dataset. + let mut stmt = conn.prepare( + r#" + SELECT dataset, format, op + FROM compression_times + GROUP BY dataset, format, op + "#, + )?; + let rows = stmt.query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + )) + })?; + for row in rows { + let (dataset, format, op) = row?; + let chart = chart_name_compression_time(&format, &op, &dataset); + groups + .entry("Compression".to_string()) + .or_default() + .insert(normalize_chart(&chart)); + } + + let mut stmt = conn.prepare( + r#" + SELECT dataset, format + FROM compression_sizes + GROUP BY dataset, format + "#, + )?; + let rows = stmt.query_map([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + })?; + for row in rows { + let (_dataset, format) = row?; + let chart = chart_name_compression_size(&format); + groups + .entry("Compression Size".to_string()) + .or_default() + .insert(normalize_chart(&chart)); + } + + let mut stmt = conn.prepare( + r#" + SELECT DISTINCT dataset + FROM random_access_times + "#, + )?; + let rows = stmt.query_map([], |row| row.get::<_, String>(0))?; + for row in rows { + let dataset = row?; + groups + .entry("Random Access".to_string()) + .or_default() + .insert(normalize_chart(&dataset)); + } + + Ok(groups) +} + +fn fetch_v2_metadata(server: &str) -> Result>> { + let url = format!("{}/api/metadata", server.trim_end_matches('/')); + let body = reqwest::blocking::get(&url) + .with_context(|| format!("GET {url}"))? + .error_for_status() + .with_context(|| format!("non-2xx from {url}"))? + .json::() + .with_context(|| format!("parsing {url} as v2 /api/metadata"))?; + let mut out: BTreeMap> = BTreeMap::new(); + for (name, group) in body.groups { + let charts = group + .charts + .into_iter() + .map(|c| normalize_chart(&c.name)) + .collect(); + out.insert(name, charts); + } + Ok(out) +} + +fn diff( + v2: &BTreeMap>, + v3: &BTreeMap>, +) -> VerifyReport { + let mut report = VerifyReport::default(); + let v2_keys: BTreeSet<&String> = v2.keys().collect(); + let v3_keys: BTreeSet<&String> = v3.keys().collect(); + for g in v2_keys.intersection(&v3_keys) { + report.matched_groups.push((**g).clone()); + let v2_charts = &v2[*g]; + let v3_charts = &v3[*g]; + if v2_charts.len() != v3_charts.len() { + report.chart_diffs.push(ChartDiff { + group: (**g).clone(), + v2_count: v2_charts.len(), + v3_count: v3_charts.len(), + }); + } + } + for g in v3_keys.difference(&v2_keys) { + report.only_in_v3.push((**g).clone()); + } + for g in v2_keys.difference(&v3_keys) { + report.only_in_v2.push((**g).clone()); + } + report.matched_groups.sort(); + report.only_in_v3.sort(); + report.only_in_v2.sort(); + report +} + +fn display_query_group(dataset: &str, scale_factor: Option<&str>, storage: &str) -> String { + let suite = QUERY_SUITES + .iter() + .find(|s| s.prefix.eq_ignore_ascii_case(dataset)) + .copied(); + match suite { + Some(suite) if suite.fan_out => { + let storage_disp = match storage { + "s3" | "S3" => "S3", + _ => "NVMe", + }; + let sf = scale_factor.unwrap_or("1"); + format!("{} ({}) (SF={})", suite.display_name, storage_disp, sf) + } + Some(suite) => suite.display_name.to_string(), + None => format!("{dataset} ({storage})"), + } +} + +fn chart_name_query(dataset: &str, query_idx: i32) -> String { + let suite = QUERY_SUITES + .iter() + .find(|s| s.prefix.eq_ignore_ascii_case(dataset)) + .copied(); + match suite { + Some(suite) => format!("{} Q{}", suite.query_prefix, query_idx), + None => format!("{} Q{}", dataset.to_uppercase(), query_idx), + } +} + +fn chart_name_compression_time(format: &str, op: &str, _dataset: &str) -> String { + // Re-derive the v2 chart name (the metric, not the dataset) so we + // can compare. v2's chart axis is the metric; series is the + // dataset. v3 inverts that. For structural comparison, we project + // back to v2's per-chart key. + match (format, op) { + ("vortex-file-compressed", "encode") => "COMPRESS TIME".into(), + ("vortex-file-compressed", "decode") => "DECOMPRESS TIME".into(), + ("parquet", "encode") => "PARQUET RS ZSTD COMPRESS TIME".into(), + ("parquet", "decode") => "PARQUET RS ZSTD DECOMPRESS TIME".into(), + ("lance", "encode") => "LANCE COMPRESS TIME".into(), + ("lance", "decode") => "LANCE DECOMPRESS TIME".into(), + _ => format!("{} {} TIME", format.to_uppercase(), op.to_uppercase()), + } +} + +fn chart_name_compression_size(format: &str) -> String { + match format { + "vortex-file-compressed" => "VORTEX SIZE".into(), + "parquet" => "PARQUET SIZE".into(), + "lance" => "LANCE SIZE".into(), + _ => format!("{} SIZE", format.to_uppercase()), + } +} + +/// Strip casing and `_-` differences between v2 and v3 chart names. +/// v2 displays uppercase; v3 stores raw values. Comparing in this +/// canonical form is enough for structural verification. +fn normalize_chart(s: &str) -> String { + s.trim() + .to_uppercase() + .replace(['_', '-'], " ") + .split_whitespace() + .collect::>() + .join(" ") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalize_chart_canonicalizes() { + assert_eq!(normalize_chart("taxi/take"), "TAXI/TAKE"); + assert_eq!(normalize_chart("TAXI/TAKE"), "TAXI/TAKE"); + assert_eq!(normalize_chart("tpc-h q1"), "TPC H Q1"); + assert_eq!(normalize_chart("tpc h q1"), "TPC H Q1"); + } + + #[test] + fn display_query_group_handles_fan_out() { + assert_eq!( + display_query_group("tpch", Some("10"), "s3"), + "TPC-H (S3) (SF=10)" + ); + assert_eq!( + display_query_group("tpch", Some("100"), "nvme"), + "TPC-H (NVMe) (SF=100)" + ); + assert_eq!( + display_query_group("clickbench", None, "nvme"), + "Clickbench" + ); + } +} diff --git a/benchmarks-website/migrate/tests/classifier.rs b/benchmarks-website/migrate/tests/classifier.rs new file mode 100644 index 00000000000..cddca0c517c --- /dev/null +++ b/benchmarks-website/migrate/tests/classifier.rs @@ -0,0 +1,439 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Classifier behavior pinned by representative v2 names from each +//! group in `benchmarks-website/server.js`'s `getGroup`. + +use rstest::rstest; +use serde_json::json; +use vortex_bench_migrate::classifier::Outcome; +use vortex_bench_migrate::classifier::Skip; +use vortex_bench_migrate::classifier::V3Bin; +use vortex_bench_migrate::classifier::classify; +use vortex_bench_migrate::classifier::classify_outcome; +use vortex_bench_migrate::classifier::format_query; +use vortex_bench_migrate::classifier::rename_engine; +use vortex_bench_migrate::v2::V2Record; + +fn record(name: &str) -> V2Record { + V2Record { + name: name.to_string(), + commit_id: Some("deadbeef".into()), + unit: Some("ns".into()), + value: Some(json!(123)), + storage: None, + dataset: None, + all_runtimes: None, + env_triple: None, + } +} + +fn record_with_storage_and_sf(name: &str, storage: &str, suite: &str, sf: &str) -> V2Record { + let mut r = record(name); + r.storage = Some(storage.into()); + r.dataset = Some(json!({ suite: { "scale_factor": sf } })); + r +} + +#[rstest] +#[case::clickbench( + "clickbench_q07/datafusion:parquet", + V3Bin::Query { + dataset: "clickbench".into(), + dataset_variant: None, + scale_factor: None, + query_idx: 7, + storage: "nvme".into(), + engine: "datafusion".into(), + format: "parquet".into(), + }, +)] +#[case::clickbench_vortex_renamed( + "clickbench_q12/datafusion:vortex-file-compressed", + V3Bin::Query { + dataset: "clickbench".into(), + dataset_variant: None, + scale_factor: None, + query_idx: 12, + storage: "nvme".into(), + engine: "datafusion".into(), + format: "vortex-file-compressed".into(), + }, +)] +#[case::statpopgen( + "statpopgen_q3/datafusion:parquet", + V3Bin::Query { + dataset: "statpopgen".into(), + dataset_variant: None, + scale_factor: None, + query_idx: 3, + storage: "nvme".into(), + engine: "datafusion".into(), + format: "parquet".into(), + }, +)] +#[case::polarsignals( + "polarsignals_q1/duckdb:parquet", + V3Bin::Query { + dataset: "polarsignals".into(), + dataset_variant: None, + scale_factor: None, + query_idx: 1, + storage: "nvme".into(), + engine: "duckdb".into(), + format: "parquet".into(), + }, +)] +fn non_fan_out_query_records(#[case] name: &str, #[case] expected: V3Bin) { + let r = record(name); + assert_eq!(classify(&r), Some(expected)); +} + +#[rstest] +#[case::tpch_s3_sf100( + "tpch_q01/datafusion:parquet", + "S3", + "tpch", + "100", + V3Bin::Query { + dataset: "tpch".into(), + dataset_variant: None, + scale_factor: Some("100".into()), + query_idx: 1, + storage: "s3".into(), + engine: "datafusion".into(), + format: "parquet".into(), + }, +)] +#[case::tpch_nvme_sf1( + "tpch_q22/duckdb:vortex-file-compressed", + "NVMe", + "tpch", + "1", + V3Bin::Query { + dataset: "tpch".into(), + dataset_variant: None, + scale_factor: Some("1".into()), + query_idx: 22, + storage: "nvme".into(), + engine: "duckdb".into(), + format: "vortex-file-compressed".into(), + }, +)] +#[case::tpcds_nvme_sf10( + "tpcds_q05/datafusion:vortex-file-compressed", + "NVMe", + "tpcds", + "10", + V3Bin::Query { + dataset: "tpcds".into(), + dataset_variant: None, + scale_factor: Some("10".into()), + query_idx: 5, + storage: "nvme".into(), + engine: "datafusion".into(), + format: "vortex-file-compressed".into(), + }, +)] +fn fan_out_query_records( + #[case] name: &str, + #[case] storage: &str, + #[case] suite: &str, + #[case] sf: &str, + #[case] expected: V3Bin, +) { + let r = record_with_storage_and_sf(name, storage, suite, sf); + assert_eq!(classify(&r), Some(expected)); +} + +#[rstest] +#[case::random_access_4_part( + "random-access/taxi/take/parquet-tokio-local-disk", + V3Bin::RandomAccess { + dataset: "taxi/take".into(), + format: "parquet".into(), + }, +)] +#[case::random_access_4_part_vortex( + "random-access/chimp/take/vortex-tokio-local-disk", + V3Bin::RandomAccess { + dataset: "chimp/take".into(), + format: "vortex-file-compressed".into(), + }, +)] +#[case::random_access_2_part_legacy( + "random-access/parquet-tokio-local-disk", + V3Bin::RandomAccess { + dataset: "random access".into(), + format: "parquet".into(), + }, +)] +#[case::random_access_4_part_lance( + "random-access/taxi/take/lance-tokio-local-disk", + V3Bin::RandomAccess { + dataset: "taxi/take".into(), + format: "lance".into(), + }, +)] +fn random_access_records(#[case] name: &str, #[case] expected: V3Bin) { + let r = record(name); + assert_eq!(classify(&r), Some(expected)); +} + +#[rstest] +#[case::compress_time_vortex( + "compress time/clickbench", + V3Bin::CompressionTime { + dataset: "clickbench".into(), + dataset_variant: None, + format: "vortex-file-compressed".into(), + op: "encode".into(), + }, +)] +#[case::decompress_time_vortex( + "decompress time/tpch_lineitem", + V3Bin::CompressionTime { + dataset: "tpch_lineitem".into(), + dataset_variant: None, + format: "vortex-file-compressed".into(), + op: "decode".into(), + }, +)] +#[case::parquet_compress( + "parquet_rs-zstd compress time/clickbench", + V3Bin::CompressionTime { + dataset: "clickbench".into(), + dataset_variant: None, + format: "parquet".into(), + op: "encode".into(), + }, +)] +#[case::lance_decompress( + "lance decompress time/clickbench", + V3Bin::CompressionTime { + dataset: "clickbench".into(), + dataset_variant: None, + format: "lance".into(), + op: "decode".into(), + }, +)] +fn compression_time_records(#[case] name: &str, #[case] expected: V3Bin) { + let r = record(name); + assert_eq!(classify(&r), Some(expected)); +} + +#[rstest] +#[case::vortex_size( + "vortex size/clickbench", + V3Bin::CompressionSize { + dataset: "clickbench".into(), + dataset_variant: None, + format: "vortex-file-compressed".into(), + }, +)] +#[case::vortex_file_compressed_size_normalizes( + "vortex-file-compressed size/clickbench", + V3Bin::CompressionSize { + dataset: "clickbench".into(), + dataset_variant: None, + format: "vortex-file-compressed".into(), + }, +)] +#[case::parquet_size( + "parquet size/clickbench", + V3Bin::CompressionSize { + dataset: "clickbench".into(), + dataset_variant: None, + format: "parquet".into(), + }, +)] +#[case::lance_size( + "lance size/tpch_lineitem", + V3Bin::CompressionSize { + dataset: "tpch_lineitem".into(), + dataset_variant: None, + format: "lance".into(), + }, +)] +fn compression_size_records(#[case] name: &str, #[case] expected: V3Bin) { + let r = record(name); + assert_eq!(classify(&r), Some(expected)); +} + +#[rstest] +#[case::ratio_vortex_parquet("vortex:parquet-zstd ratio compress time/clickbench")] +#[case::ratio_vortex_lance("vortex:lance ratio decompress time/clickbench")] +#[case::ratio_size_vortex_parquet("vortex:parquet-zstd size/clickbench")] +#[case::ratio_size_vortex_raw("vortex:raw size/clickbench")] +#[case::throughput("compress throughput/clickbench")] +#[case::nonsense_prefix("not-a-known-bench/series")] +fn unmapped_records_yield_none(#[case] name: &str) { + let r = record(name); + assert_eq!( + classify(&r), + None, + "expected {name:?} to classify as None (drop)", + ); +} + +#[test] +fn parquet_zstd_size_is_deprecated() { + // `parquet-zstd` is not on the v3 emitter's format allowlist, so + // historical `parquet-zstd size/...` records bucket under + // Skip::Deprecated and don't render as orphan charts in v3. + let r = record("parquet-zstd size/clickbench"); + assert!(matches!( + classify_outcome(&r), + Outcome::Skip(Skip::Deprecated) + )); +} + +#[test] +fn vortex_parquet_zstd_ratio_is_intentional_skip() { + let r = record("vortex:parquet-zstd ratio compress time/clickbench"); + assert!(matches!( + classify_outcome(&r), + Outcome::Skip(Skip::DerivedRatio) + )); +} + +#[test] +fn vortex_parquet_zst_typo_ratio_is_intentional_skip() { + // `parquet-zst` (no trailing `d`) was emitted by some v2 runs. + // Both spellings should classify as derived ratios. + for name in [ + "vortex:parquet-zst ratio compress time/clickbench", + "vortex:parquet-zst ratio decompress time/clickbench", + ] { + let r = record(name); + assert!( + matches!(classify_outcome(&r), Outcome::Skip(Skip::DerivedRatio)), + "{name:?} should be DerivedRatio", + ); + } +} + +#[test] +fn throughput_is_intentional_skip() { + let r = record("compress throughput/clickbench"); + assert!(matches!( + classify_outcome(&r), + Outcome::Skip(Skip::Throughput) + )); +} + +#[test] +fn unknown_prefix_is_unknown() { + let r = record("not-a-known-bench/series"); + assert!(matches!(classify_outcome(&r), Outcome::Unknown)); +} + +#[test] +fn gharchive_q00_is_deprecated() { + // gharchive isn't on the v3 query-suite allowlist, so historical + // gharchive query records bucket as Skip::Deprecated. + let r = record("gharchive_q00/datafusion:parquet"); + assert!(matches!( + classify_outcome(&r), + Outcome::Skip(Skip::Deprecated) + )); +} + +#[test] +fn fineweb_q00_classifies() { + // fineweb is on V3_QUERY_SUITES (still emitted by v3 CI per + // .github/workflows/sql-benchmarks.yml's `fineweb` matrix entry), + // so historical fineweb records ingest like any other suite. + let r = record("fineweb_q00/datafusion:parquet"); + assert!(matches!( + classify_outcome(&r), + Outcome::Bin(V3Bin::Query { .. }) + )); +} + +#[test] +fn memory_record_is_historical_memory_skip() { + // v2 emitted `_q_memory/:` records that + // carry top-level memory fields V2Record doesn't deserialize. + // Skip them with a known variant so they don't trip the 5% gate. + let r = record("clickbench_q07_memory/datafusion:parquet"); + assert!(matches!( + classify_outcome(&r), + Outcome::Skip(Skip::HistoricalMemory) + )); +} + +#[test] +fn tpch_compression_size_carries_scale_factor() { + // The data.json.gz "vortex size/tpch" path needs to derive + // dataset_variant from the v2 record's `dataset` object, the same + // way the file-sizes path does. Otherwise SF=10 rows from the two + // sources never collide on `mid` and produce duplicate rows. + let mut r = record("vortex size/tpch"); + r.dataset = Some(serde_json::json!({ "tpch": { "scale_factor": "10" } })); + let outcome = classify_outcome(&r); + let Outcome::Bin(V3Bin::CompressionSize { + dataset, + dataset_variant, + format, + }) = outcome + else { + panic!("expected Bin(CompressionSize), got {outcome:?}"); + }; + assert_eq!(dataset, "tpch"); + assert_eq!(dataset_variant, Some("10".into())); + assert_eq!(format, "vortex-file-compressed"); +} + +#[test] +fn tpch_compression_size_drops_default_scale_factor() { + // SF "1.0" matches the file-sizes path's filter and collapses to + // dataset_variant: None. + let mut r = record("vortex size/tpch"); + r.dataset = Some(serde_json::json!({ "tpch": { "scale_factor": "1.0" } })); + let outcome = classify_outcome(&r); + let Outcome::Bin(V3Bin::CompressionSize { + dataset_variant, .. + }) = outcome + else { + panic!("expected Bin(CompressionSize), got {outcome:?}"); + }; + assert_eq!(dataset_variant, None); +} + +#[test] +fn engine_casing_lowercased() { + // Older v2 records emitted display-case engines like `DataFusion` + // and `DuckDB`. The classifier lowercases at push time so dedup + // collapses display-case rows into the canonical lowercase ones. + let r = record("clickbench_q07/DataFusion:parquet"); + let outcome = classify_outcome(&r); + let Outcome::Bin(V3Bin::Query { engine, format, .. }) = outcome else { + panic!("expected Bin(Query), got {outcome:?}"); + }; + assert_eq!(engine, "datafusion"); + assert_eq!(format, "parquet"); +} + +#[test] +fn rename_engine_pins_canonical_outputs() { + assert_eq!(rename_engine("vortex-tokio-local-disk"), "vortex-nvme"); + assert_eq!( + rename_engine("datafusion:vortex-file-compressed"), + "datafusion:vortex" + ); + assert_eq!(rename_engine("LANCE"), "lance"); +} + +#[test] +fn format_query_pins_v2_display() { + assert_eq!(format_query("clickbench_q00"), "CLICKBENCH Q0"); + assert_eq!(format_query("tpch_q22"), "TPC-H Q22"); + assert_eq!(format_query("tpcds_q42"), "TPC-DS Q42"); + assert_eq!(format_query("polarsignals_q1"), "POLARSIGNALS Q1"); + // Names that don't match a suite fall back to upper + " " replace. + assert_eq!( + format_query("vortex-file-compressed size"), + "VORTEX FILE COMPRESSED SIZE" + ); +} diff --git a/benchmarks-website/migrate/tests/end_to_end.rs b/benchmarks-website/migrate/tests/end_to_end.rs new file mode 100644 index 00000000000..210092a4058 --- /dev/null +++ b/benchmarks-website/migrate/tests/end_to_end.rs @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Inline JSONL fixtures driven through the full migration into a +//! tempdir DuckDB. No live S3. + +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use duckdb::Connection; +use flate2::Compression; +use flate2::write::GzEncoder; +use tempfile::TempDir; +use vortex_bench_migrate::migrate; +use vortex_bench_migrate::source::Source; + +const COMMITS_JSONL: &str = r#"{"id":"deadbeef","timestamp":"2026-04-25T00:00:00Z","message":"fixture commit","author":{"name":"A","email":"a@example.com"},"committer":{"name":"C","email":"c@example.com"},"tree_id":"abcd0001","url":"https://example.com/commit/deadbeef"} +"#; + +const DATA_JSONL: &str = r#"{"name":"clickbench_q07/datafusion:parquet","commit_id":"deadbeef","unit":"ns","value":42000,"all_runtimes":[41000,42000,43000]} +{"name":"compress time/clickbench","commit_id":"deadbeef","unit":"ns","value":99} +{"name":"vortex size/clickbench","commit_id":"deadbeef","unit":"bytes","value":1024} +{"name":"random-access/taxi/take/parquet-tokio-local-disk","commit_id":"deadbeef","unit":"ns","value":777,"all_runtimes":[700,777,800]} +"#; + +/// Build a local-source fixture directory. Caller supplies the contents +/// of `commits.json`, `data.json.gz`, and any number of +/// `file-sizes-*.json.gz` files (name → contents). +fn build_fixture(commits: &str, data: &str, file_sizes: &[(&str, &str)]) -> TempDir { + let dir = TempDir::new().expect("tempdir"); + write_text(&dir.path().join("commits.json"), commits); + write_gz(&dir.path().join("data.json.gz"), data); + for (name, body) in file_sizes { + write_gz(&dir.path().join(name), body); + } + dir +} + +fn write_text(path: &Path, body: &str) { + let mut f = File::create(path).unwrap(); + f.write_all(body.as_bytes()).unwrap(); +} + +fn write_gz(path: &Path, body: &str) { + let f = File::create(path).unwrap(); + let mut gz = GzEncoder::new(f, Compression::default()); + gz.write_all(body.as_bytes()).unwrap(); + gz.finish().unwrap(); +} + +#[test] +fn migrate_inline_fixture_populates_each_table() { + let src_dir = build_fixture(COMMITS_JSONL, DATA_JSONL, &[]); + let target_dir = TempDir::new().unwrap(); + let target = target_dir.path().join("v3.duckdb"); + + let summary = migrate::run(&Source::Local(src_dir.path().into()), &target).unwrap(); + + assert_eq!(summary.records_read, 4, "summary={summary}"); + assert_eq!(summary.uncategorized, 0, "summary={summary}"); + assert_eq!(summary.commits_inserted, 1); + assert_eq!(summary.query_inserted, 1); + assert_eq!(summary.compression_time_inserted, 1); + assert_eq!(summary.compression_size_inserted, 1); + assert_eq!(summary.random_access_inserted, 1); + + let conn = Connection::open(&target).unwrap(); + let count = |table: &str| -> i64 { + conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |r| r.get(0)) + .unwrap() + }; + assert_eq!(count("commits"), 1); + assert_eq!(count("query_measurements"), 1); + assert_eq!(count("compression_times"), 1); + assert_eq!(count("compression_sizes"), 1); + assert_eq!(count("random_access_times"), 1); + + // Spot-check the v3 column values for each kind. + let (engine, format, query_idx, value_ns): (String, String, i32, i64) = conn + .query_row( + "SELECT engine, format, query_idx, value_ns FROM query_measurements", + [], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)), + ) + .unwrap(); + assert_eq!(engine, "datafusion"); + assert_eq!(format, "parquet"); + assert_eq!(query_idx, 7); + assert_eq!(value_ns, 42000); + + let (dataset, format, op): (String, String, String) = conn + .query_row( + "SELECT dataset, format, op FROM compression_times", + [], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), + ) + .unwrap(); + assert_eq!(dataset, "clickbench"); + assert_eq!(format, "vortex-file-compressed"); + assert_eq!(op, "encode"); + + let (dataset, format, value_bytes): (String, String, i64) = conn + .query_row( + "SELECT dataset, format, value_bytes FROM compression_sizes", + [], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), + ) + .unwrap(); + assert_eq!(dataset, "clickbench"); + assert_eq!(format, "vortex-file-compressed"); + assert_eq!(value_bytes, 1024); + + let (dataset, format): (String, String) = conn + .query_row("SELECT dataset, format FROM random_access_times", [], |r| { + Ok((r.get(0)?, r.get(1)?)) + }) + .unwrap(); + assert_eq!(dataset, "taxi/take"); + assert_eq!(format, "parquet"); +} + +#[test] +fn dedup_collision_keeps_one_row() { + // Two data.json.gz lines whose query-measurement dim columns are + // identical (same commit / dataset / engine / format / query_idx, + // and `storage` collapses to "nvme" since `storage` is unset). + // Different `value`s. The accumulator's HashSet + // should drop the second one and bump `summary.deduped`. + const DATA: &str = r#"{"name":"clickbench_q07/datafusion:parquet","commit_id":"deadbeef","unit":"ns","value":111} +{"name":"clickbench_q07/datafusion:parquet","commit_id":"deadbeef","unit":"ns","value":222} +"#; + + let src_dir = build_fixture(COMMITS_JSONL, DATA, &[]); + let target_dir = TempDir::new().unwrap(); + let target = target_dir.path().join("v3.duckdb"); + + let summary = migrate::run(&Source::Local(src_dir.path().into()), &target).unwrap(); + + assert_eq!(summary.records_read, 2, "summary={summary}"); + assert_eq!(summary.query_inserted, 1, "summary={summary}"); + assert_eq!(summary.deduped, 1, "summary={summary}"); + + let conn = Connection::open(&target).unwrap(); + let n: i64 = conn + .query_row("SELECT COUNT(*) FROM query_measurements", [], |r| r.get(0)) + .unwrap(); + assert_eq!(n, 1); +} + +#[test] +fn dedup_with_conflicting_value_ns_is_counted() { + // Same dim columns, different `value`s. Dedup keeps the first + // and bumps `deduped_with_conflict` because the dropped row's + // value_ns differed from the kept row's. This is the signal we + // care about when watching for silent value-corruption across + // duplicated v2 emissions. + const DATA: &str = r#"{"name":"clickbench_q07/datafusion:parquet","commit_id":"deadbeef","unit":"ns","value":111} +{"name":"clickbench_q07/datafusion:parquet","commit_id":"deadbeef","unit":"ns","value":222} +"#; + + let src_dir = build_fixture(COMMITS_JSONL, DATA, &[]); + let target_dir = TempDir::new().unwrap(); + let target = target_dir.path().join("v3.duckdb"); + + let summary = migrate::run(&Source::Local(src_dir.path().into()), &target).unwrap(); + + assert_eq!(summary.deduped, 1, "summary={summary}"); + assert_eq!(summary.deduped_with_conflict, 1, "summary={summary}"); +} + +#[test] +fn dedup_with_matching_value_ns_does_not_count_conflict() { + // Same dim columns AND identical `value`s. Dedup still drops the + // duplicate, but `deduped_with_conflict` stays 0. + const DATA: &str = r#"{"name":"clickbench_q07/datafusion:parquet","commit_id":"deadbeef","unit":"ns","value":111} +{"name":"clickbench_q07/datafusion:parquet","commit_id":"deadbeef","unit":"ns","value":111} +"#; + + let src_dir = build_fixture(COMMITS_JSONL, DATA, &[]); + let target_dir = TempDir::new().unwrap(); + let target = target_dir.path().join("v3.duckdb"); + + let summary = migrate::run(&Source::Local(src_dir.path().into()), &target).unwrap(); + + assert_eq!(summary.deduped, 1, "summary={summary}"); + assert_eq!(summary.deduped_with_conflict, 0, "summary={summary}"); +} + +#[test] +fn compression_size_data_and_file_sizes_merge() { + // A `vortex size/tpch` record from data.json.gz and a + // file-sizes-tpch-nvme.json.gz row covering the same (commit, + // dataset, format, SF) tuple should produce the *same* + // measurement_id so the in-memory accumulator merges them into + // one row instead of two. + // + // Both sources use scale_factor "1.0", which both code paths + // filter out → dataset_variant: None on both sides → matching mid. + const DATA: &str = r#"{"name":"vortex size/tpch","commit_id":"deadbeef","unit":"bytes","value":200,"dataset":{"tpch":{"scale_factor":"1.0"}}} +"#; + const FILE_SIZES: &str = r#"{"commit_id":"deadbeef","benchmark":"tpch","scale_factor":"1.0","format":"vortex-file-compressed","file":"part-0.vortex","size_bytes":100} +"#; + + let src_dir = build_fixture( + COMMITS_JSONL, + DATA, + &[("file-sizes-tpch-nvme.json.gz", FILE_SIZES)], + ); + let target_dir = TempDir::new().unwrap(); + let target = target_dir.path().join("v3.duckdb"); + + let summary = migrate::run(&Source::Local(src_dir.path().into()), &target).unwrap(); + + assert_eq!(summary.compression_size_inserted, 1, "summary={summary}"); + + let conn = Connection::open(&target).unwrap(); + let (n, value_bytes): (i64, i64) = conn + .query_row( + "SELECT COUNT(*), SUM(value_bytes) FROM compression_sizes", + [], + |r| Ok((r.get(0)?, r.get(1)?)), + ) + .unwrap(); + assert_eq!(n, 1); + // data.json.gz seeds value_bytes=200, file-sizes adds 100. + assert_eq!(value_bytes, 300); +} + +#[test] +fn file_sizes_sum_into_one_row() { + // Two file-sizes rows sharing (commit, benchmark, format, + // scale_factor) and value_bytes 100 + 200 must collapse to a + // single compression_sizes row with 300. + const FILE_SIZES: &str = r#"{"commit_id":"deadbeef","benchmark":"clickbench","scale_factor":"1.0","format":"vortex-file-compressed","file":"part-0.vortex","size_bytes":100} +{"commit_id":"deadbeef","benchmark":"clickbench","scale_factor":"1.0","format":"vortex-file-compressed","file":"part-1.vortex","size_bytes":200} +"#; + + let src_dir = build_fixture( + COMMITS_JSONL, + "", + &[("file-sizes-clickbench.json.gz", FILE_SIZES)], + ); + let target_dir = TempDir::new().unwrap(); + let target = target_dir.path().join("v3.duckdb"); + + let summary = migrate::run(&Source::Local(src_dir.path().into()), &target).unwrap(); + + assert_eq!(summary.file_size_inserted, 2, "summary={summary}"); + assert_eq!(summary.compression_size_inserted, 1, "summary={summary}"); + + let conn = Connection::open(&target).unwrap(); + let n: i64 = conn + .query_row("SELECT COUNT(*) FROM compression_sizes", [], |r| r.get(0)) + .unwrap(); + assert_eq!(n, 1); + let value_bytes: i64 = conn + .query_row("SELECT value_bytes FROM compression_sizes", [], |r| { + r.get(0) + }) + .unwrap(); + assert_eq!(value_bytes, 300); +} From 15cab9b5f7401204b6b48528f34154e14aac2f74 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 27 Apr 2026 09:53:51 -0400 Subject: [PATCH 11/12] add .bench-env to gitignore Signed-off-by: Connor Tsui --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6a996cf96cc..bcc8ef746ee 100644 --- a/.gitignore +++ b/.gitignore @@ -244,4 +244,4 @@ trace*.pb vortex-python/.benchmarks/ # For local benchmarks website server and things like the WAL **.duckdb* - +.bench-env From 7efbcacd2daf441af72b6505deaf550a6ceac41d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 14:15:42 +0000 Subject: [PATCH 12/12] chore(benchmarks-website): remove v2 code now that v3 is live The v3 Rust server (benchmarks-website/server/) and migrator (benchmarks-website/migrate/) have replaced the v2 Express/Vite stack. Signed-off-by: Claude --- .../workflows/publish-benchmarks-website.yml | 38 - benchmarks-website/Dockerfile | 16 - benchmarks-website/docker-compose.yml | 29 - benchmarks-website/ec2-init.txt | 70 - benchmarks-website/index.html | 36 - benchmarks-website/migrate/src/classifier.rs | 6 +- benchmarks-website/migrate/src/lib.rs | 2 +- .../migrate/tests/classifier.rs | 2 +- benchmarks-website/package-lock.json | 2298 ----------------- benchmarks-website/package.json | 32 - .../public/android-chrome-192x192.png | Bin 8637 -> 0 bytes .../public/android-chrome-512x512.png | Bin 27388 -> 0 bytes .../public/apple-touch-icon.png | Bin 7639 -> 0 bytes benchmarks-website/public/favicon-16x16.png | Bin 362 -> 0 bytes benchmarks-website/public/favicon-32x32.png | Bin 873 -> 0 bytes benchmarks-website/public/favicon.ico | Bin 15406 -> 0 bytes benchmarks-website/public/site.webmanifest | 19 - .../public/vortex_black_nobg.svg | 69 - .../public/vortex_white_nobg.svg | 70 - benchmarks-website/server.js | 775 ------ benchmarks-website/src/App.jsx | 295 --- benchmarks-website/src/api.js | 41 - .../src/components/BenchmarkSection.jsx | 147 -- .../src/components/BenchmarkSummary.jsx | 129 - .../src/components/ChartContainer.jsx | 664 ----- benchmarks-website/src/components/Header.jsx | 110 - benchmarks-website/src/components/Modal.jsx | 476 ---- benchmarks-website/src/components/Sidebar.jsx | 59 - benchmarks-website/src/config.js | 276 -- benchmarks-website/src/main.jsx | 10 - benchmarks-website/src/styles/index.css | 1319 ---------- benchmarks-website/src/utils.js | 103 - benchmarks-website/vite.config.js | 19 - 33 files changed, 5 insertions(+), 7105 deletions(-) delete mode 100644 .github/workflows/publish-benchmarks-website.yml delete mode 100644 benchmarks-website/Dockerfile delete mode 100644 benchmarks-website/docker-compose.yml delete mode 100644 benchmarks-website/ec2-init.txt delete mode 100644 benchmarks-website/index.html delete mode 100644 benchmarks-website/package-lock.json delete mode 100644 benchmarks-website/package.json delete mode 100644 benchmarks-website/public/android-chrome-192x192.png delete mode 100644 benchmarks-website/public/android-chrome-512x512.png delete mode 100644 benchmarks-website/public/apple-touch-icon.png delete mode 100644 benchmarks-website/public/favicon-16x16.png delete mode 100644 benchmarks-website/public/favicon-32x32.png delete mode 100644 benchmarks-website/public/favicon.ico delete mode 100644 benchmarks-website/public/site.webmanifest delete mode 100644 benchmarks-website/public/vortex_black_nobg.svg delete mode 100644 benchmarks-website/public/vortex_white_nobg.svg delete mode 100644 benchmarks-website/server.js delete mode 100644 benchmarks-website/src/App.jsx delete mode 100644 benchmarks-website/src/api.js delete mode 100644 benchmarks-website/src/components/BenchmarkSection.jsx delete mode 100644 benchmarks-website/src/components/BenchmarkSummary.jsx delete mode 100644 benchmarks-website/src/components/ChartContainer.jsx delete mode 100644 benchmarks-website/src/components/Header.jsx delete mode 100644 benchmarks-website/src/components/Modal.jsx delete mode 100644 benchmarks-website/src/components/Sidebar.jsx delete mode 100644 benchmarks-website/src/config.js delete mode 100644 benchmarks-website/src/main.jsx delete mode 100644 benchmarks-website/src/styles/index.css delete mode 100644 benchmarks-website/src/utils.js delete mode 100644 benchmarks-website/vite.config.js diff --git a/.github/workflows/publish-benchmarks-website.yml b/.github/workflows/publish-benchmarks-website.yml deleted file mode 100644 index e7eeefb8ecc..00000000000 --- a/.github/workflows/publish-benchmarks-website.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Publish Benchmarks Website - -on: - push: - branches: [develop] - paths: - - "benchmarks-website/**" - -jobs: - publish: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v6 - - - name: Log in to GHCR - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Build and push - uses: docker/build-push-action@v7 - with: - context: ./benchmarks-website - platforms: linux/arm64 - push: true - tags: ghcr.io/${{ github.repository }}/benchmarks-website:latest diff --git a/benchmarks-website/Dockerfile b/benchmarks-website/Dockerfile deleted file mode 100644 index 1f87a7148b5..00000000000 --- a/benchmarks-website/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:24-alpine AS build -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci -COPY . . -RUN npm run build - -FROM node:24-alpine -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci --omit=dev -COPY --from=build /app/dist ./dist -COPY server.js . -COPY src/config.js ./src/config.js -EXPOSE 3000 -CMD ["node", "server.js"] diff --git a/benchmarks-website/docker-compose.yml b/benchmarks-website/docker-compose.yml deleted file mode 100644 index b97482a230a..00000000000 --- a/benchmarks-website/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -services: - benchmarks-website: - image: ghcr.io/vortex-data/vortex/benchmarks-website:latest - ports: - - "80:3000" - restart: unless-stopped - - vortex-bench-server: - image: ghcr.io/vortex-data/vortex/vortex-bench-server:latest - ports: - - "3001:3000" - environment: - VORTEX_BENCH_DB: "/app/data/bench.duckdb" - VORTEX_BENCH_BIND: "0.0.0.0:3000" - VORTEX_BENCH_LOG: "info,vortex_bench_server=debug" - env_file: - - /etc/vortex-bench/secrets.env - volumes: - - /opt/benchmarks-website/data:/app/data - restart: unless-stopped - - watchtower: - image: containrrr/watchtower - volumes: - - /var/run/docker.sock:/var/run/docker.sock - environment: - - WATCHTOWER_POLL_INTERVAL=60 - - WATCHTOWER_CLEANUP=true - restart: unless-stopped diff --git a/benchmarks-website/ec2-init.txt b/benchmarks-website/ec2-init.txt deleted file mode 100644 index 4e1377cc014..00000000000 --- a/benchmarks-website/ec2-init.txt +++ /dev/null @@ -1,70 +0,0 @@ - 1. Install Docker - # Amazon Linux 2023 - sudo yum install -y docker - sudo systemctl enable --now docker - sudo usermod -aG docker $USER - newgrp docker - - 2. Install Docker Compose plugin - sudo mkdir -p /usr/local/lib/docker/cli-plugins - sudo curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-aarch64 -o /usr/local/lib/docker/cli-plugins/docker-compose - sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose - - 3. Set up and start the app - sudo mkdir -p /opt/benchmarks-website - sudo cp docker-compose.yml /opt/benchmarks-website/ - cd /opt/benchmarks-website - docker compose up -d - - ==================================================================== - v3 (vortex-bench-server) — additive setup, runs alongside v2 - ==================================================================== - - v2 stays on port 80 until DNS is flipped. v3 runs on port 3001 from - the same docker-compose.yml on this host. - - 4. Create the bearer-token env file (root:root, mode 600) - sudo mkdir -p /etc/vortex-bench - sudo install -m 600 -o root -g root /dev/null /etc/vortex-bench/secrets.env - # Edit and set INGEST_BEARER_TOKEN=: - sudo vi /etc/vortex-bench/secrets.env - # File contents: - # INGEST_BEARER_TOKEN= - - 5. Create the EBS-backed DuckDB data directory - # Assumes an EBS volume is already mounted at /opt/benchmarks-website/data. - sudo mkdir -p /opt/benchmarks-website/data - sudo chown root:root /opt/benchmarks-website/data - sudo chmod 755 /opt/benchmarks-website/data - - 6. Pull and start v3 (watchtower already polls ghcr.io for refreshes) - cd /opt/benchmarks-website - docker compose pull vortex-bench-server - docker compose up -d vortex-bench-server - # Smoke-check on the host: - curl -sf http://127.0.0.1:3001/health || echo "v3 not responding" - - 7. Install the daily DuckDB backup cron - # Copy the backup script from the repo checkout to a stable location. - sudo install -m 755 -o root -g root \ - benchmarks-website/server/scripts/backup.sh \ - /usr/local/bin/vortex-bench-backup.sh - # Cron entry: 06:00 UTC daily, after the nightly bench finishes. - sudo tee /etc/cron.d/vortex-bench-backup >/dev/null <<'CRON' - 0 6 * * * root /usr/local/bin/vortex-bench-backup.sh >> /var/log/vortex-bench-backup.log 2>&1 - CRON - sudo chmod 644 /etc/cron.d/vortex-bench-backup - # The instance IAM role already permits writes to - # s3://vortex-ci-benchmark-results/ (same role v2's cat-s3.sh uses). - - 8. Bearer-token rotation procedure - # When rotating INGEST_BEARER_TOKEN: - # a. Generate a new token (e.g. `openssl rand -hex 32`). - # b. Update the GitHub Actions Environment secret INGEST_BEARER_TOKEN - # so CI dual-writes use the new value. - # c. On this EC2 host, edit the env file and restart only the v3 - # container so v2 traffic on port 80 is unaffected: - # sudo vi /etc/vortex-bench/secrets.env - # cd /opt/benchmarks-website - # docker compose up -d --force-recreate vortex-bench-server - # d. Verify with `curl` against /health and a token-gated endpoint. \ No newline at end of file diff --git a/benchmarks-website/index.html b/benchmarks-website/index.html deleted file mode 100644 index e475f3ad254..00000000000 --- a/benchmarks-website/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Vortex Benchmarks - - - - - - - - - - - - - - - - - -
- - - diff --git a/benchmarks-website/migrate/src/classifier.rs b/benchmarks-website/migrate/src/classifier.rs index 8a17b31fcd2..dfbdb75705b 100644 --- a/benchmarks-website/migrate/src/classifier.rs +++ b/benchmarks-website/migrate/src/classifier.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors //! Bug-for-bug port of v2's `getGroup`, `formatQuery`, and -//! `normalizeChartName` from `benchmarks-website/server.js`, plus the +//! `normalizeChartName` from the v2 Express server, plus the //! mapping from v2 group + name pattern to a v3 fact-table bin. //! //! The v2 classifier was the source of truth for what historical @@ -114,7 +114,7 @@ pub struct QuerySuite { pub skip: bool, } -/// Group a v2 record falls into. Mirrors `getGroup` in `server.js`, +/// Group a v2 record falls into. Mirrors v2's `getGroup`, /// including the fan-out group naming for TPC-H/TPC-DS. #[derive(Debug, Clone, PartialEq, Eq)] pub enum V2Group { @@ -296,7 +296,7 @@ pub fn get_group(record: &V2Record) -> Option { } /// Group + chart + series breakdown for a v2 record, using the same -/// rules `server.js` applies in `refresh()`. Equivalent to v2's +/// rules the v2 server applies in `refresh()`. Equivalent to v2's /// `(group, chartName, seriesName)` triple after rename / skip rules. #[derive(Debug, Clone, PartialEq, Eq)] pub struct V2Classification { diff --git a/benchmarks-website/migrate/src/lib.rs b/benchmarks-website/migrate/src/lib.rs index 5e8d9c64907..f02db73b4b7 100644 --- a/benchmarks-website/migrate/src/lib.rs +++ b/benchmarks-website/migrate/src/lib.rs @@ -6,7 +6,7 @@ //! //! The v2 dataset is JSONL of bare benchmark records keyed by name string. //! v3 uses five typed fact tables with explicit dim columns. This crate -//! ports v2's `getGroup` classifier (in `benchmarks-website/server.js`) +//! ports v2's `getGroup` classifier from the v2 Express server //! bug-for-bug so that historical rows survive the migration with the //! same group / chart / series structure as the live v2 server. //! diff --git a/benchmarks-website/migrate/tests/classifier.rs b/benchmarks-website/migrate/tests/classifier.rs index cddca0c517c..e4bb3991940 100644 --- a/benchmarks-website/migrate/tests/classifier.rs +++ b/benchmarks-website/migrate/tests/classifier.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors //! Classifier behavior pinned by representative v2 names from each -//! group in `benchmarks-website/server.js`'s `getGroup`. +//! group in v2's `getGroup` classifier. use rstest::rstest; use serde_json::json; diff --git a/benchmarks-website/package-lock.json b/benchmarks-website/package-lock.json deleted file mode 100644 index d140b73d225..00000000000 --- a/benchmarks-website/package-lock.json +++ /dev/null @@ -1,2298 +0,0 @@ -{ - "name": "vortex-benchmarks-website", - "version": "2.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "vortex-benchmarks-website", - "version": "2.0.0", - "dependencies": { - "chart.js": "^4.4.4", - "chartjs-plugin-zoom": "^2.0.1", - "downsample": "^1.4.0", - "hammerjs": "^2.0.8", - "lucide-react": "^0.577.0", - "react": "^18.3.1", - "react-chartjs-2": "^5.2.0", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "concurrently": "^8.2.2", - "vite": "^6.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "license": "MIT" - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/hammerjs": { - "version": "2.0.46", - "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", - "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", - "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", - "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001790", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", - "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, - "node_modules/chartjs-plugin-zoom": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", - "integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==", - "license": "MIT", - "dependencies": { - "@types/hammerjs": "^2.0.45", - "hammerjs": "^2.0.8" - }, - "peerDependencies": { - "chart.js": ">=3.2.0" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", - "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "date-fns": "^2.30.0", - "lodash": "^4.17.21", - "rxjs": "^7.8.1", - "shell-quote": "^1.8.1", - "spawn-command": "0.0.2", - "supports-color": "^8.1.1", - "tree-kill": "^1.2.2", - "yargs": "^17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": "^14.13.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/downsample": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/downsample/-/downsample-1.4.0.tgz", - "integrity": "sha512-teYPhUPxqwtyICt47t1mP/LjhbRV/ghuKb/LmFDbcZ0CjqFD31tn6rVLZoeCEa1xr8+f2skW8UjRiLiGIKQE4w==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.343", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", - "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/hammerjs": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", - "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-chartjs-2": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", - "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", - "license": "MIT", - "peerDependencies": { - "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/spawn-command": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", - "dev": true - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - } - } -} diff --git a/benchmarks-website/package.json b/benchmarks-website/package.json deleted file mode 100644 index 6cda687b838..00000000000 --- a/benchmarks-website/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "vortex-benchmarks-website", - "version": "2.0.0", - "type": "module", - "scripts": { - "dev": "concurrently \"npm run server\" \"npm run vite\"", - "vite": "vite", - "server": "node server.js", - "build": "vite build", - "preview": "vite preview" - }, - "engines": { - "node": ">=18.0.0" - }, - "dependencies": { - "chart.js": "^4.4.4", - "chartjs-plugin-zoom": "^2.0.1", - "downsample": "^1.4.0", - "hammerjs": "^2.0.8", - "lucide-react": "^0.577.0", - "react": "^18.3.1", - "react-chartjs-2": "^5.2.0", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "concurrently": "^8.2.2", - "vite": "^6.0.0" - } -} diff --git a/benchmarks-website/public/android-chrome-192x192.png b/benchmarks-website/public/android-chrome-192x192.png deleted file mode 100644 index 1089ab6a060ed20c85a2d3778cf7ba41e14fbd36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8637 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 I73*vN0W&Frc>n+a diff --git a/benchmarks-website/public/apple-touch-icon.png b/benchmarks-website/public/apple-touch-icon.png deleted file mode 100644 index 2b34da9ee0c8da782b1c891c315987054dfde580..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7639 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 diff --git a/benchmarks-website/public/favicon.ico b/benchmarks-website/public/favicon.ico deleted file mode 100644 index 3d4e2ff9fe0b846a37a498f1324d0e7fe9777920..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeI2Rjd?C5QgXG-UoPqaFL)75bQvZgS$N74#9#GNN@@6?j8ccf`kNjcMI+W4J0@T zJmGTCy8G2(mOXQ3cV}mIL&)7uHapW&^>$UtMD2&-Ma_aVCplB>4LHhWJJ#*Zcl#i->&p@cZf;>!rZkO9?yv{Q2{t z=-ZCpyLYc#xNt$oO`SSbE?>SJjE3#_Q>RWzr%s(@{P^)QWy%y;uwa3lK7CrCYbmM5 zU%GTj`;QwpPPT2^CQFwt6`rAIVhW&b@WT zDOmB%1z+*IexDr3GL~Ju`bn?Y;{3fz(A#*=^8fPXi+ug+nvCm#=Cs?kZ7a8K-7;Sz z{ddg&&6_v2w7Ylju4~E1j~{K@!&ExvU$$&n*|~G4EMB}==Fgw+Cp&%mbQv{jlvJ-? z-Cq~MgJb@WA3v4~6)MPp0Rse?w{G29-oAY+8#Zi^p+kpCp+bfHY@a@TD&@+`^xRxw{?z<8a0yQ#f$4ZdWY`c zxpPM%^uvz$bLOa2sgl|m^h=d0r5IMMSfM&Gd-iOpQKN=p3?b`)Y5O0;@_+sMwUjAS zM)0X-LB4|q4N^U$9iPD22KgZOXV0FQUBmr{D92PyIRG5Ju6qqDcz5|H&ne|Mz~qxcuYt_acF~ z{)CYpXP|H2zJ=-Kx;f_ZCzr^XiWpZ6N=%3b;v`1IIr!D9SAo^hdiI3hiA>k5StFdQ zIlD*UTxXrPC;Z5koa6WJ-!;BM491F)JScIAef##w=+UDii9uMg+P!0-DRtk|m3*UcEX6R_ucKAn{oz`j}#H^1k0v71=BNi6bIo z;?ql(EYX-5d5gn`56koC&oypO%#!$;8C!x2@tiJQx(MIx?Af!CF;4F7N&Y57;<((G zAl_KCXi?2E^ytw;a^}n_W51c{;Ub)Ytp4lr*T_s$VG`7eqv|b*8qRk ztXWmh$)#k=mQB*8O)CQj4wNxt#%Mf|zQ`N7PntAI^IV@kxk_2P!cWW-n?;w=2V_c| zvu@qGs_Xgk<&)#bkE=M*A=>dnYuB#TKHRk+Z`Q6|JH78he`3TgO2qhy#}g;Vmtc?R zbgNdaR8Q8eTc>vV;K2jGEn>rr;UC-u3l`M)cHzQ>HLjW?M-IX6>Eog}!VhnJ1GdUK z#JwQo&;1ze5Hd{oBRuD)8U!HHGX7??O}u1A~uOVFsERJF(*%+lz#pCso%t=ko%!Shx9w* zm)VoB7SPv0afKf_PM9!3^@iLMzL10_#*U)g3dEHz7KuC&V38~!Ajpf2mJAq zCr^Sv3|Z|8Klxqc$hyNk@cZxHy;EG&p&$5D_M+@@z?V65X3YU}o}{n!oDdRa@J3ho z@jK+E**Aj^TLTX=MYi1IY}l}&egk|H^I^W&D)(2=d588H9C>n!EBxl(t83S;DRP?V z1NhiS;9KyIoV$4DZVdNZxjVsrXW_zyPVV7E9Y4N-JI3|u)k}dLM(8r@7-vdkjc?~$ zu^-|tO|D$IROh*4%eU*+ty?M@;Q(9OUCG}iU3>|9PWBh9OJ-pnoCUBK?ih0~ zhCa;8Ly0PWuTK1C01W>;Rh$RahTfo?A?^ zQO6Hq)=>5b>}6tHVuBx;;)AYSxe{aensBoS_1z8cZIO; diff --git a/benchmarks-website/public/site.webmanifest b/benchmarks-website/public/site.webmanifest deleted file mode 100644 index ba9480d90de..00000000000 --- a/benchmarks-website/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Vortex Benchmarks", - "short_name": "Benchmarks", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/benchmarks-website/public/vortex_black_nobg.svg b/benchmarks-website/public/vortex_black_nobg.svg deleted file mode 100644 index f8210102998..00000000000 --- a/benchmarks-website/public/vortex_black_nobg.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/benchmarks-website/public/vortex_white_nobg.svg b/benchmarks-website/public/vortex_white_nobg.svg deleted file mode 100644 index 767362ad28f..00000000000 --- a/benchmarks-website/public/vortex_white_nobg.svg +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/benchmarks-website/server.js b/benchmarks-website/server.js deleted file mode 100644 index a9af96234fe..00000000000 --- a/benchmarks-website/server.js +++ /dev/null @@ -1,775 +0,0 @@ -import http from "http"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import zlib from "zlib"; -import readline from "readline"; -import { Readable } from "stream"; -import { LTTB } from "downsample"; -import { QUERY_SUITES, FAN_OUT_GROUPS, ENGINE_RENAMES } from "./src/config.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Configuration -const PORT = process.env.PORT || 3000; -const DATA_URL = - process.env.DATA_URL || - "https://vortex-ci-benchmark-results.s3.amazonaws.com/data.json.gz"; -const COMMITS_URL = - process.env.COMMITS_URL || - "https://vortex-ci-benchmark-results.s3.amazonaws.com/commits.json"; -const REFRESH_INTERVAL = process.env.REFRESH_INTERVAL || 5 * 60 * 1000; -const MAX_POINTS = 200; -const USE_LOCAL_DATA = process.env.USE_LOCAL_DATA === "true"; - -// Benchmark groups: non-query groups + simple suites + fan-out suites -const GROUPS = [ - "Random Access", - "Compression", - "Compression Size", - ...QUERY_SUITES.filter((s) => !s.skip && !s.fanOut).map((s) => s.displayName), - ...FAN_OUT_GROUPS, -]; - -const MIME = { - ".html": "text/html", - ".js": "application/javascript", - ".css": "text/css", - ".json": "application/json", - ".png": "image/png", - ".jpg": "image/jpeg", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".webmanifest": "application/manifest+json", -}; - -let store = { - commits: [], - groups: {}, - metadata: null, - downsampled: {}, - lastUpdated: null, -}; - -// Utilities -const rename = (s) => ENGINE_RENAMES[s.toLowerCase()] || ENGINE_RENAMES[s] || s; -const geoMean = (arr) => - arr.length - ? Math.pow( - arr.reduce((a, v) => a * v, 1), - 1 / arr.length, - ) - : null; - -// Categorize benchmarks based on name patterns and metadata -function getGroup(benchmark) { - const name = benchmark.name; - const lower = name.toLowerCase(); - - // Random Access: "random-access/..." or "random access/..." - if ( - lower.startsWith("random-access/") || - lower.startsWith("random access/") - ) { - return "Random Access"; - } - - // Compression Size: size measurements - if ( - lower.startsWith("vortex size/") || - lower.startsWith("vortex-file-compressed size/") || - lower.startsWith("parquet size/") || - lower.startsWith("lance size/") || - lower.includes(":raw size/") || - lower.includes(":parquet-zstd size/") || - lower.includes(":lance size/") - ) { - return "Compression Size"; - } - - // Compression: compress/decompress time and ratio measurements - if ( - lower.startsWith("compress time/") || - lower.startsWith("decompress time/") || - lower.startsWith("parquet_rs-zstd compress") || - lower.startsWith("parquet_rs-zstd decompress") || - lower.startsWith("lance compress") || - lower.startsWith("lance decompress") || - lower.startsWith("vortex:lance ratio") || - lower.startsWith("vortex:parquet-zstd ratio") || - lower.startsWith("vortex:raw ratio") - ) { - return "Compression"; - } - - // SQL query suites: match "{prefix}_q..." or "{prefix}/..." - for (const suite of QUERY_SUITES) { - if ( - !lower.startsWith(suite.prefix + "_q") && - !lower.startsWith(suite.prefix + "/") - ) - continue; - if (suite.skip) return null; - if (!suite.fanOut) return suite.displayName; - // Fan-out suites: expand by storage and scale factor - const storage = benchmark.storage?.toUpperCase() === "S3" ? "S3" : "NVMe"; - const rawSf = benchmark.dataset?.[suite.datasetKey]?.scale_factor; - const sf = rawSf ? Math.round(parseFloat(rawSf)) : 1; - return `${suite.displayName} (${storage}) (SF=${sf})`; - } - - return null; -} - -// Format query name for display: "{prefix}_q00" -> "{QUERY_PREFIX} Q0" -function formatQuery(q) { - const lower = q.toLowerCase(); - for (const suite of QUERY_SUITES) { - if (suite.skip) continue; - const m = lower.match(new RegExp(`^${suite.prefix}[_ ]?q(\\d+)`, "i")); - if (m) return `${suite.queryPrefix} Q${parseInt(m[1], 10)}`; - } - return q.toUpperCase().replace(/[_-]/g, " "); -} - -function normalizeChartName(group, chartName) { - if (group === "Compression Size" && chartName === "VORTEX FILE COMPRESSED SIZE") { - return "VORTEX SIZE"; - } - return chartName; -} - -// LTTB downsampling -function lttbIndices(seriesMap, target) { - const keys = [...seriesMap.keys()]; - if (!keys.length) return []; - const len = seriesMap.get(keys[0])?.length || 0; - if (len <= target) return [...Array(len).keys()]; - - const avg = Array(len); - for (let i = 0; i < len; i++) { - let sum = 0, - n = 0; - for (const arr of seriesMap.values()) { - const v = arr[i]?.value ?? arr[i]; - if (v != null && !isNaN(v)) { - sum += v; - n++; - } - } - avg[i] = [i, n ? sum / n : 0]; - } - - const idx = LTTB(avg, target).map((p) => Math.round(p[0])); - if (!idx.includes(0)) idx.unshift(0); - if (!idx.includes(len - 1)) idx.push(len - 1); - return idx.sort((a, b) => a - b); -} - -function downsample(data, factor) { - const target = Math.ceil(data.commits.length / factor); - if (target >= data.commits.length) return data; - - const idx = lttbIndices(data.series, target); - const series = new Map(); - for (const [k, v] of data.series) - series.set( - k, - idx.map((i) => v[i]), - ); - - return { - ...data, - commits: idx.map((i) => data.commits[i]), - series, - originalLength: data.commits.length, - }; -} - -// Data fetching — streams response body directly instead of buffering -async function fetchJsonl(url) { - const res = await fetch(url); - if (!res.ok) throw new Error(`Fetch failed: ${url} ${res.status}`); - return new Promise((resolve, reject) => { - const results = []; - const rl = readline.createInterface({ - input: Readable.fromWeb(res.body), - crlfDelay: Infinity, - }); - rl.on("line", (l) => { - if (l.trim()) - try { - results.push(JSON.parse(l)); - } catch {} - }); - rl.on("close", () => resolve(results)); - rl.on("error", reject); - }); -} - -function readLocalJsonl(fp) { - return new Promise((resolve, reject) => { - const results = []; - const rl = readline.createInterface({ - input: fs.createReadStream(fp), - crlfDelay: Infinity, - }); - rl.on("line", (l) => { - if (l.trim()) - try { - results.push(JSON.parse(l)); - } catch {} - }); - rl.on("close", () => resolve(results)); - rl.on("error", reject); - }); -} - -// Stream benchmark data record-by-record without buffering the entire dataset -async function forEachBenchmark(callback) { - let stream; - if (USE_LOCAL_DATA) { - stream = fs.createReadStream(path.join(__dirname, "sample/data.json")); - } else { - const res = await fetch(DATA_URL); - if (!res.ok) throw new Error(`Fetch failed: ${DATA_URL} ${res.status}`); - stream = Readable.fromWeb(res.body).pipe(zlib.createGunzip()); - } - return new Promise((resolve, reject) => { - const rl = readline.createInterface({ input: stream, crlfDelay: Infinity }); - rl.on("line", (l) => { - if (l.trim()) - try { - callback(JSON.parse(l)); - } catch {} - }); - rl.on("close", resolve); - rl.on("error", reject); - }); -} - -// Main data processing -async function refresh() { - console.log("Refreshing data..."); - const t0 = Date.now(); - - try { - // Load commits first (small dataset, must be fully in memory for indexing) - const commitsArr = USE_LOCAL_DATA - ? await readLocalJsonl(path.join(__dirname, "sample/commits.json")) - : await fetchJsonl(COMMITS_URL); - - // Build commit index (O(1) lookup) - const commitMap = new Map(commitsArr.map((c) => [c.id, c])); - const commits = commitsArr.sort( - (a, b) => new Date(a.timestamp) - new Date(b.timestamp), - ); - const commitIdx = new Map(commits.map((c, i) => [c.id, i])); - - const groups = Object.fromEntries(GROUPS.map((g) => [g, new Map()])); - let missing = 0; - let benchmarkCount = 0; - const uncategorized = new Set(); - - // Stream benchmarks one record at a time to avoid loading all into memory - await forEachBenchmark((b) => { - benchmarkCount++; - const commit = b.commit || commitMap.get(b.commit_id); - if (!commit) { - missing++; - return; - } - - const group = getGroup(b); - if (!group) { - uncategorized.add(b.name.split("/")[0]); - return; - } - if (!groups[group]) return; - - // Random access names have the form: random-access/{dataset}/{pattern}/{format} - // Historical random access names: random-access/{format} - // Other benchmarks use: {query}/{series} - let seriesName, chartName; - const parts = b.name.split("/"); - if (group === "Random Access" && parts.length === 4) { - chartName = `${parts[1]}/${parts[2]}`.toUpperCase().replace(/[_-]/g, " "); - seriesName = rename(parts[3] || "default"); - } else if (group === "Random Access" && parts.length === 2) { - chartName = "RANDOM ACCESS"; - seriesName = rename(parts[1] || "default"); - } else { - seriesName = rename(parts[1] || "default"); - chartName = formatQuery(parts[0]); - } - chartName = normalizeChartName(group, chartName); - if (chartName.includes("PARQUET-UNC")) return; - - // Skip throughput metrics (keep only time/size) - if (b.name.includes(" throughput")) return; - - let unit = b.unit; - if (!unit) { - if (b.name.toLowerCase().includes(" size/")) unit = "bytes"; - else if (b.name.toLowerCase().includes(" ratio ")) unit = "ratio"; - else unit = "ns"; - } - - const sortPos = parts[0].match(/q(\d+)$/i)?.[1] - ? parseInt(RegExp.$1, 10) - : 0; - const idx = commitIdx.get(commit.id); - if (idx === undefined) return; - - let chart = groups[group].get(chartName); - if (!chart) { - let displayUnit = unit; - if (unit === "ns") displayUnit = "ms/iter"; - else if (unit === "bytes") displayUnit = "MiB"; - chart = { - sort_position: sortPos, - commits, - unit: displayUnit, - series: new Map(), - }; - groups[group].set(chartName, chart); - } - - if (!chart.series.has(seriesName)) { - chart.series.set(seriesName, Array(commits.length).fill(null)); - } - - // Convert values: ns -> ms, bytes -> MiB - let val = b.value; - if (unit === "ns" && typeof val === "number") { - val = val / 1e6; // ns to ms - } else if (unit === "bytes" && typeof val === "number") { - val = val / (1024 * 1024); // bytes to MiB - } - - chart.series.get(seriesName)[idx] = { value: val }; - }); - - console.log( - `Processed ${benchmarkCount} benchmarks, ${commitsArr.length} commits`, - ); - - // Log uncategorized benchmarks for debugging - if (uncategorized.size > 0) { - console.log( - `Uncategorized benchmark prefixes (${uncategorized.size}):`, - [...uncategorized].slice(0, 20).join(", "), - ); - } - - // Trim leading empty commits - let firstIdx = commits.length; - for (const gc of Object.values(groups)) { - for (const cd of gc.values()) { - for (const sd of cd.series.values()) { - const i = sd.findIndex((d) => d !== null); - if (i !== -1 && i < firstIdx) firstIdx = i; - } - } - } - - if (firstIdx > 0 && firstIdx < commits.length) { - console.log(`Trimming ${firstIdx} empty commits`); - commits.splice(0, firstIdx); - for (const gc of Object.values(groups)) { - for (const cd of gc.values()) { - cd.commits = commits; - for (const [k, v] of cd.series) cd.series.set(k, v.slice(firstIdx)); - } - } - } - - // Sort charts within groups - for (const gc of Object.values(groups)) { - const sorted = [...gc.entries()].sort( - (a, b) => - a[1].sort_position - b[1].sort_position || a[0].localeCompare(b[0]), - ); - gc.clear(); - for (const [k, v] of sorted) gc.set(k, v); - } - - // Precompute downsampled versions - const downsampled = {}; - for (const [gn, gc] of Object.entries(groups)) { - downsampled[gn] = {}; - for (const [cn, cd] of gc) { - downsampled[gn][cn] = { - "1x": cd, - "2x": downsample(cd, 2), - "4x": downsample(cd, 4), - "8x": downsample(cd, 8), - }; - } - } - - // Count charts per group for logging - const groupCounts = Object.entries(groups) - .map(([n, g]) => `${n}: ${g.size}`) - .filter((s) => !s.endsWith(": 0")); - console.log("Charts per group:", groupCounts.join(", ")); - - store = { - commits, - groups, - metadata: buildMeta(groups, commits), - downsampled, - lastUpdated: new Date().toISOString(), - }; - console.log( - `Refresh done in ${Date.now() - t0}ms (${missing} missing commits)`, - ); - } catch (e) { - console.error("Refresh error:", e); - } -} - -// Summary calculations -function latestIdx(chart) { - for (let i = chart.commits.length - 1; i >= 0; i--) { - for (const s of chart.series.values()) if (s[i]?.value != null) return i; - } - return -1; -} - -function calcSummary(name, charts) { - if (name === "Random Access") { - for (const q of charts.values()) { - const i = latestIdx(q); - if (i === -1) continue; - const vals = new Map(); - for (const [n, d] of q.series) - if (d[i]?.value != null) vals.set(n, d[i].value); - if (!vals.size) continue; - const min = Math.min(...vals.values()); - return { - type: "randomAccess", - title: "Random Access Performance", - rankings: [...vals] - .map(([n, t]) => ({ name: n, time: t, ratio: t / min })) - .sort((a, b) => a.time - b.time), - explanation: "Random access time | Ratio to fastest (lower is better)", - }; - } - return null; - } - - if (name === "Compression") { - const cc = charts.get("VORTEX:PARQUET ZSTD RATIO COMPRESS TIME"); - const dc = charts.get("VORTEX:PARQUET ZSTD RATIO DECOMPRESS TIME"); - if (!cc && !dc) return null; - const i = latestIdx(cc || dc); - if (i === -1) return null; - const collect = (c) => - c - ? [...c.series] - .filter(([n]) => !n.toLowerCase().includes("wide table")) - .map(([, d]) => d[i]?.value) - .filter((v) => v > 0) - .map((v) => 1 / v) - : []; - return { - type: "compression", - title: "Compression Throughput vs Parquet", - compressRatio: geoMean(collect(cc)), - decompressRatio: geoMean(collect(dc)), - datasetCount: collect(cc).length, - explanation: - "Inverse geomean of Vortex/Parquet ratios (higher is better)", - }; - } - - if (name === "Compression Size") { - const c = charts.get("VORTEX:PARQUET ZSTD SIZE"); - if (!c) return null; - const i = latestIdx(c); - if (i === -1) return null; - const ratios = [...c.series] - .filter(([n]) => !n.toLowerCase().includes("wide table")) - .map(([, d]) => d[i]?.value) - .filter((v) => v > 0); - return ratios.length - ? { - type: "compressionSize", - title: "Compression Size Summary", - minRatio: Math.min(...ratios), - meanRatio: geoMean(ratios), - maxRatio: Math.max(...ratios), - datasetCount: ratios.length, - explanation: - "Geomean of Vortex/Parquet size ratios (lower is better)", - } - : null; - } - - if ( - QUERY_SUITES.some( - (s) => - !s.skip && - (name === s.displayName || name.startsWith(s.displayName + " ")), - ) - ) { - const all = new Map(); - for (const q of charts.values()) - for (const n of q.series.keys()) if (!all.has(n)) all.set(n, new Map()); - for (const [qn, qd] of charts) { - for (const [sn, sd] of qd.series) { - for (let i = sd.length - 1; i >= 0; i--) { - if (sd[i]?.value != null) { - all.get(sn).set(qn, sd[i].value); - break; - } - } - } - } - if (!all.size) return null; - - const scores = new Map(); - for (const [sn, qr] of all) { - let total = 0, - max = 0; - for (const v of qr.values()) { - total += v; - max = Math.max(max, v); - } - const penalty = Math.max(300000, max) * 2; - const ratios = []; - for (const qn of charts.keys()) { - let base = Infinity; - for (const m of all.values()) - if (m.has(qn)) base = Math.min(base, m.get(qn)); - if (base < Infinity) - ratios.push((10 + (qr.get(qn) ?? penalty)) / (10 + base)); - } - if (ratios.length) - scores.set(sn, { score: geoMean(ratios), totalRuntime: total }); - } - - return scores.size - ? { - type: "queryBenchmark", - title: "Performance Summary", - rankings: [...scores] - .map(([n, d]) => ({ name: n, ...d })) - .sort((a, b) => a.score - b.score), - explanation: - "Geomean of query time ratio to fastest (lower is better)", - } - : null; - } - return null; -} - -function buildMeta(groups, commits) { - const meta = {}; - for (const [gn, gc] of Object.entries(groups)) { - const charts = [...gc].map(([cn, cd]) => { - const latest = {}; - for (const [sn, sd] of cd.series) { - for (let i = sd.length - 1; i >= 0; i--) - if (sd[i]?.value != null) { - latest[sn] = sd[i].value; - break; - } - } - return { - name: cn, - unit: cd.unit, - series: [...cd.series.keys()], - sortPosition: cd.sort_position, - totalPoints: cd.commits.length, - latestValues: latest, - }; - }); - meta[gn] = { - charts, - totalCharts: charts.length, - hasData: charts.length > 0, - summary: calcSummary(gn, gc), - }; - } - return { - groups: meta, - totalCommits: commits.length, - commits: commits.map((c) => ({ - id: c.id, - message: c.message?.split("\n")[0] || "", - timestamp: c.timestamp, - author: c.author?.name || "Unknown", - })), - lastUpdated: new Date().toISOString(), - }; -} - -// HTTP handlers -const json = (res, code, data) => { - res.writeHead(code, { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*", - }); - res.end(JSON.stringify(data)); -}; - -function serveFile(res, fp) { - fs.readFile(fp, (err, data) => { - if (err) { - res.writeHead(err.code === "ENOENT" ? 404 : 500); - return res.end(err.code === "ENOENT" ? "Not Found" : "Error"); - } - const ext = path.extname(fp).toLowerCase(); - const hdrs = { "Content-Type": MIME[ext] || "application/octet-stream" }; - if (ext === ".js") { - hdrs["Cache-Control"] = "no-cache"; - hdrs["Pragma"] = "no-cache"; - } - res.writeHead(200, hdrs); - res.end(data); - }); -} - -function handleData(res, group, chart, start, end, last, startIdx, endIdx) { - if (!store.downsampled) return json(res, 503, { error: "Loading" }); - const gd = store.downsampled[group]; - if (!gd) return json(res, 404, { error: "Group not found" }); - const cv = gd[chart]; - if (!cv) return json(res, 404, { error: "Chart not found" }); - - const full = cv["1x"]; - const ts = (c) => - typeof c?.timestamp === "number" - ? c.timestamp - : new Date(c?.timestamp).getTime(); - - let si = 0, - ei = full.commits.length - 1; - - // Support "last=N" parameter to get the last N commits - if (last && !start && !end && startIdx === null && endIdx === null) { - const n = parseInt(last, 10); - if (n > 0 && n < full.commits.length) { - si = full.commits.length - n; - } - } else if (startIdx !== null || endIdx !== null) { - // Support index-based range (startIdx, endIdx) - if (startIdx !== null) si = Math.max(0, parseInt(startIdx, 10)); - if (endIdx !== null) - ei = Math.min(full.commits.length - 1, parseInt(endIdx, 10)); - } else { - // Timestamp-based range - if (start) { - const t = +start, - i = full.commits.findIndex((c) => ts(c) >= t); - if (i !== -1) si = i; - } - if (end) { - const t = +end; - for (let i = ei; i >= 0; i--) - if (ts(full.commits[i]) <= t) { - ei = i; - break; - } - } - } - - const len = ei - si + 1; - const ver = - len <= MAX_POINTS - ? "1x" - : len <= MAX_POINTS * 2 - ? "2x" - : len <= MAX_POINTS * 4 - ? "4x" - : "8x"; - const cd = cv[ver]; - const val = (d) => d?.value ?? (typeof d === "number" ? d : null); - - let commits, series; - if (ver === "1x") { - commits = full.commits.slice(si, ei + 1); - series = Object.fromEntries( - [...full.series].map(([n, d]) => [n, d.slice(si, ei + 1).map(val)]), - ); - } else { - const s = +ver[0], - dsi = Math.floor(si / s), - dei = Math.min(Math.ceil(ei / s), cd.commits.length - 1); - commits = cd.commits.slice(dsi, dei + 1); - series = Object.fromEntries( - [...cd.series].map(([n, d]) => [n, d.slice(dsi, dei + 1).map(val)]), - ); - } - - json(res, 200, { - group, - chart, - unit: cd.unit, - downsampleLevel: ver, - originalLength: full.commits.length, - requestedRange: { startIndex: si, endIndex: ei, length: len }, - commits: commits.map((c) => ({ - id: c.id, - message: c.message?.split("\n")[0] || "", - timestamp: c.timestamp, - author: c.author?.name || "Unknown", - url: c.url, - })), - series, - }); -} - -const server = http.createServer((req, res) => { - const [path_, qs] = req.url.split("?"); - const params = new URLSearchParams(qs || ""); - - if (req.method === "OPTIONS") { - res.writeHead(204, { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type", - }); - return res.end(); - } - - if (path_ === "/api/metadata") - return store.metadata - ? json(res, 200, store.metadata) - : json(res, 503, { error: "Loading" }); - - if (path_.startsWith("/api/data/")) { - const p = path_.slice(10).split("/"); - return handleData( - res, - decodeURIComponent(p[0] || ""), - decodeURIComponent(p.slice(1).join("/") || ""), - params.get("start"), - params.get("end"), - params.get("last"), - params.has("startIdx") ? params.get("startIdx") : null, - params.has("endIdx") ? params.get("endIdx") : null, - ); - } - - const fp = path.join(__dirname, "dist", path_ === "/" ? "index.html" : path_); - if (!fp.startsWith(__dirname) || fp.includes("/sample/")) { - res.writeHead(403); - return res.end("Forbidden"); - } - serveFile(res, fp); -}); - -async function start() { - console.log("Starting server..."); - await refresh(); - setInterval(refresh, REFRESH_INTERVAL); - server.listen(PORT, () => console.log(`Server at http://localhost:${PORT}`)); -} - -start().catch(console.error); diff --git a/benchmarks-website/src/App.jsx b/benchmarks-website/src/App.jsx deleted file mode 100644 index 0df05bebf01..00000000000 --- a/benchmarks-website/src/App.jsx +++ /dev/null @@ -1,295 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import Header from './components/Header'; -import Sidebar from './components/Sidebar'; -import BenchmarkSection from './components/BenchmarkSection'; -import Modal from './components/Modal'; -import { fetchMetadata } from './api'; -import { BENCHMARK_CONFIGS, CATEGORY_TAGS } from './config'; - -export default function App() { - const [metadata, setMetadata] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [expandedGroups, setExpandedGroups] = useState(new Set()); - const [sidebarOpen, setSidebarOpen] = useState(false); - const [categoryFilter, setCategoryFilter] = useState('all'); - const [searchFilter, setSearchFilter] = useState(''); - const [viewMode, setViewMode] = useState('grid'); - const [modalChart, setModalChart] = useState(null); - const [showBackToTop, setShowBackToTop] = useState(false); - const metadataFetched = useRef(false); - - useEffect(() => { - if (metadataFetched.current) return; - metadataFetched.current = true; - - async function loadMetadata() { - try { - const data = await fetchMetadata(); - setMetadata(data); - const params = new URLSearchParams(window.location.search); - if (params.get('expanded') === 'true' && data?.groups) { - setExpandedGroups(new Set(Object.keys(data.groups))); - } - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - } - loadMetadata(); - }, []); - - useEffect(() => { - const handleScroll = () => { - setShowBackToTop(window.scrollY > 400); - }; - window.addEventListener('scroll', handleScroll, { passive: true }); - return () => window.removeEventListener('scroll', handleScroll); - }, []); - - // Handle hash-based navigation on page load - useEffect(() => { - if (!metadata || loading) return; - - const hash = window.location.hash; - if (hash && hash.startsWith('#group-')) { - const groupId = hash.slice(1); // Remove the '#' - const groupName = groupId.replace('group-', '').replace(/-/g, ' '); - - // Find the matching group (case-insensitive, handle hyphenated names) - const matchingGroup = Object.keys(metadata.groups).find(name => - name.replace(/\s+/g, '-') === groupId.replace('group-', '') - ); - - if (matchingGroup) { - // Expand the group - setExpandedGroups(prev => new Set([...prev, matchingGroup])); - - // Scroll to the element after a short delay to allow rendering - setTimeout(() => { - const element = document.getElementById(groupId); - if (element) { - const headerHeight = 72; - const y = element.getBoundingClientRect().top + window.scrollY - headerHeight - 16; - window.scrollTo({ top: y, behavior: 'smooth' }); - } - }, 100); - } - } - }, [metadata, loading]); - - // Get benchmark config by group name - const getBenchmarkConfig = useCallback((groupName) => { - return BENCHMARK_CONFIGS.find(c => c.name === groupName) || {}; - }, []); - - // Filter groups based on category and search - const filteredGroups = useMemo(() => { - if (!metadata?.groups) return []; - - return Object.keys(metadata.groups).filter(groupName => { - // Category filter - if (categoryFilter !== 'all') { - const tags = CATEGORY_TAGS[groupName] || []; - if (!tags.includes(categoryFilter)) return false; - } - - // Search filter - if (searchFilter) { - const searchLower = searchFilter.toLowerCase(); - const matchesGroup = groupName.toLowerCase().includes(searchLower); - const groupData = metadata.groups[groupName]; - const charts = groupData?.charts || []; - const matchesChart = charts.some(c => - c.name.toLowerCase().includes(searchLower) - ); - if (!matchesGroup && !matchesChart) return false; - } - - return true; - }); - }, [metadata, categoryFilter, searchFilter]); - - // Toggle group expansion - const toggleGroup = useCallback((groupName) => { - setExpandedGroups(prev => { - const next = new Set(prev); - if (next.has(groupName)) { - next.delete(groupName); - } else { - next.add(groupName); - } - return next; - }); - }, []); - - // Expand all groups - const expandAll = useCallback(() => { - if (metadata?.groups) { - setExpandedGroups(new Set(Object.keys(metadata.groups))); - const url = new URL(window.location); - url.searchParams.set('expanded', 'true'); - window.history.replaceState(null, '', url); - } - }, [metadata]); - - // Collapse all groups - const collapseAll = useCallback(() => { - setExpandedGroups(new Set()); - const url = new URL(window.location); - url.searchParams.delete('expanded'); - window.history.replaceState(null, '', url); - }, []); - - // Scroll to group - const scrollToGroup = useCallback((groupName) => { - const element = document.getElementById(`group-${groupName.replace(/\s+/g, '-')}`); - if (element) { - const headerHeight = 72; - const y = element.getBoundingClientRect().top + window.scrollY - headerHeight - 16; - window.scrollTo({ top: y, behavior: 'smooth' }); - } - setSidebarOpen(false); - }, []); - - // Back to top - const scrollToTop = useCallback(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - }, []); - - // Clear search - const clearFilter = useCallback(() => { - setSearchFilter(''); - setCategoryFilter('all'); - }, []); - - if (loading) { - return ( -
-
setSidebarOpen(!sidebarOpen)} - categoryFilter={categoryFilter} - onCategoryChange={setCategoryFilter} - searchFilter={searchFilter} - onSearchChange={setSearchFilter} - viewMode={viewMode} - onViewModeChange={setViewMode} - onExpandAll={expandAll} - onCollapseAll={collapseAll} - /> -
-
-
-
-

Loading benchmarks...

-
-
-
-
- ); - } - - if (error) { - return ( -
-
setSidebarOpen(!sidebarOpen)} - categoryFilter={categoryFilter} - onCategoryChange={setCategoryFilter} - searchFilter={searchFilter} - onSearchChange={setSearchFilter} - viewMode={viewMode} - onViewModeChange={setViewMode} - onExpandAll={expandAll} - onCollapseAll={collapseAll} - /> -
-
-
-

Error loading benchmarks: {error}

-
-
-
-
- ); - } - - return ( -
-
setSidebarOpen(!sidebarOpen)} - categoryFilter={categoryFilter} - onCategoryChange={setCategoryFilter} - searchFilter={searchFilter} - onSearchChange={setSearchFilter} - viewMode={viewMode} - onViewModeChange={setViewMode} - onExpandAll={expandAll} - onCollapseAll={collapseAll} - /> - -
- setSidebarOpen(false)} - onGroupClick={scrollToGroup} - onClearFilter={clearFilter} - showClearFilter={categoryFilter !== 'all' || searchFilter !== ''} - /> - -
setSidebarOpen(false)} - /> - -
- {filteredGroups.map(groupName => { - const groupData = metadata.groups[groupName] || {}; - const charts = groupData.charts || []; - const config = getBenchmarkConfig(groupName); - const isExpanded = expandedGroups.has(groupName); - - if (config.hidden) return null; - - return ( - toggleGroup(groupName)} - viewMode={viewMode} - onFullscreen={(chartData) => setModalChart(chartData)} - commitRange={metadata.totalCommits} - summary={groupData.summary} - /> - ); - })} -
-
- - {showBackToTop && ( - - )} - - {modalChart && ( - setModalChart(null)} - /> - )} -
- ); -} diff --git a/benchmarks-website/src/api.js b/benchmarks-website/src/api.js deleted file mode 100644 index 042ea7a6f0b..00000000000 --- a/benchmarks-website/src/api.js +++ /dev/null @@ -1,41 +0,0 @@ -const API_BASE = ''; - -export async function fetchMetadata() { - const response = await fetch(`${API_BASE}/api/metadata`); - if (!response.ok) throw new Error(`Failed to fetch metadata: ${response.status}`); - return response.json(); -} - -export async function fetchChartData(groupName, chartName, options = {}) { - const { startTimestamp, endTimestamp, last, startIdx, endIdx } = options; - let url = `${API_BASE}/api/data/${encodeURIComponent(groupName)}/${encodeURIComponent(chartName)}`; - const params = new URLSearchParams(); - - if (last) { - params.set('last', last); - } else if (startIdx !== undefined || endIdx !== undefined) { - // Index-based range - if (startIdx !== undefined) params.set('startIdx', startIdx); - if (endIdx !== undefined) params.set('endIdx', endIdx); - } else { - // Timestamp-based range - if (startTimestamp) { - const ts = typeof startTimestamp === 'number' - ? startTimestamp - : new Date(startTimestamp).getTime(); - params.set('start', ts); - } - if (endTimestamp) { - const ts = typeof endTimestamp === 'number' - ? endTimestamp - : new Date(endTimestamp).getTime(); - params.set('end', ts); - } - } - - if (params.toString()) url += '?' + params.toString(); - - const response = await fetch(url); - if (!response.ok) throw new Error(`Failed to fetch chart data: ${response.status}`); - return response.json(); -} diff --git a/benchmarks-website/src/components/BenchmarkSection.jsx b/benchmarks-website/src/components/BenchmarkSection.jsx deleted file mode 100644 index 706a9c27fe6..00000000000 --- a/benchmarks-website/src/components/BenchmarkSection.jsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import { Info, Link2 } from 'lucide-react'; -import ChartContainer from './ChartContainer'; -import BenchmarkSummary from './BenchmarkSummary'; -import { getBenchmarkDescription, remapChartName } from '../utils'; - -export default function BenchmarkSection({ - groupName, - charts, - config, - isExpanded, - onToggle, - viewMode, - onFullscreen, - commitRange, - summary, -}) { - const [engineFilter, setEngineFilter] = useState('all'); - const [copiedLink, setCopiedLink] = useState(false); - - // Get unique engines from chart series - const engines = useMemo(() => { - const engineSet = new Set(); - charts.forEach(chart => { - chart.series?.forEach(seriesName => { - if (seriesName.includes(':')) { - const engine = seriesName.split(':')[0].toLowerCase(); - engineSet.add(engine); - } - }); - }); - return Array.from(engineSet).sort(); - }, [charts]); - - // Filter and sort charts based on config - const filteredCharts = useMemo(() => { - if (!charts) return []; - - let result = charts.filter(chart => { - // Apply keptCharts filter - if (config.keptCharts) { - const upperName = chart.name.toUpperCase(); - return config.keptCharts.some(kept => upperName === kept.toUpperCase()); - } - return true; - }); - - // Sort by keptCharts order if specified - if (config.keptCharts) { - const orderMap = new Map(config.keptCharts.map((name, idx) => [name.toUpperCase(), idx])); - result.sort((a, b) => { - const aIdx = orderMap.get(a.name.toUpperCase()) ?? 999; - const bIdx = orderMap.get(b.name.toUpperCase()) ?? 999; - return aIdx - bIdx; - }); - } - - return result; - }, [charts, config]); - - // Copy link to clipboard - const handleCopyLink = useCallback((e) => { - e.stopPropagation(); - const url = `${window.location.origin}${window.location.pathname}#group-${groupName.replace(/\s+/g, '-')}`; - navigator.clipboard.writeText(url); - setCopiedLink(true); - setTimeout(() => setCopiedLink(false), 2000); - }, [groupName]); - - const description = getBenchmarkDescription(groupName); - const hasData = filteredCharts.length > 0; - const chartCount = filteredCharts.length; - - return ( -
-
-
- {isExpanded ? '▼' : '▶'} -
-

- {groupName} - -

- {description && ( - - - - )} -
- {chartCount} {chartCount === 1 ? 'CHART' : 'CHARTS'} -
-
-
- - {isExpanded && engines.length > 0 && ( -
- Filter by engine: - - {engines.map(engine => ( - - ))} -
- )} -
- - - - {isExpanded && ( -
- {filteredCharts.map(chart => ( - - ))} -
- )} -
- ); -} diff --git a/benchmarks-website/src/components/BenchmarkSummary.jsx b/benchmarks-website/src/components/BenchmarkSummary.jsx deleted file mode 100644 index 6bc2d4f1790..00000000000 --- a/benchmarks-website/src/components/BenchmarkSummary.jsx +++ /dev/null @@ -1,129 +0,0 @@ -import React from 'react'; -import { formatTime } from '../utils'; - -// BenchmarkSummary now uses pre-computed summary from metadata (passed via props) -// instead of fetching all chart data -export default function BenchmarkSummary({ groupName, charts, summary }) { - // Use pre-computed summary from metadata - const summaryData = summary; - - if (!summaryData) return null; - - // Query benchmarks (Clickbench, TPC-H, TPC-DS, etc.) - if (summaryData.type === 'queryBenchmark' && summaryData.rankings?.length > 0) { - return ( -
-

{summaryData.title || 'Performance Summary'}

-
- {summaryData.rankings.map((item, idx) => ( -
- #{idx + 1} - {item.name} - - {item.score.toFixed(2)}x - {formatTime(item.totalRuntime)} - -
- ))} -
-
- {summaryData.explanation || 'Score: geometric mean of query time ratio to fastest (lower is better)'} -
-
- ); - } - - if (summaryData.type === 'randomAccess' && summaryData.rankings?.length > 0) { - return ( -
-

{summaryData.title || 'Random Access Performance'}

-
- {summaryData.rankings.map((item, idx) => ( -
- #{idx + 1} - {item.name} - - {formatTime(item.time)} - {item.ratio.toFixed(2)}x - -
- ))} -
-
- {summaryData.explanation || 'Random access time | Ratio to fastest (lower is better)'} -
-
- ); - } - - if (summaryData.type === 'compression') { - return ( -
-

{summaryData.title || 'Compression Throughput vs Parquet'}

-
- {summaryData.compressRatio && ( -
- - Write Speed (Compression) - - {summaryData.compressRatio.toFixed(2)}x - -
- )} - {summaryData.decompressRatio && ( -
- 📤 - Scan Speed (Decompression) - - {summaryData.decompressRatio.toFixed(2)}x - -
- )} -
-
- {summaryData.explanation || `Inverse geometric mean of Vortex/Parquet ratios across ${summaryData.datasetCount || 'multiple'} datasets (higher is better)`} -
-
- ); - } - - if (summaryData.type === 'compressionSize' && summaryData.meanRatio) { - return ( -
-

{summaryData.title || 'Compression Size Summary'}

-
- {summaryData.minRatio && ( -
- ⬇️ - Min Size Ratio - - {summaryData.minRatio.toFixed(2)}x - -
- )} -
- 📊 - Mean Size Ratio - - {summaryData.meanRatio.toFixed(2)}x - -
- {summaryData.maxRatio && ( -
- ⬆️ - Max Size Ratio - - {summaryData.maxRatio.toFixed(2)}x - -
- )} -
-
- {summaryData.explanation || `Geometric mean of Vortex/Parquet size ratios across ${summaryData.datasetCount || 'multiple'} datasets (lower is better)`} -
-
- ); - } - - return null; -} diff --git a/benchmarks-website/src/components/ChartContainer.jsx b/benchmarks-website/src/components/ChartContainer.jsx deleted file mode 100644 index 1dfb193e836..00000000000 --- a/benchmarks-website/src/components/ChartContainer.jsx +++ /dev/null @@ -1,664 +0,0 @@ -import { - CategoryScale, - Chart as ChartJS, - Legend, - LinearScale, - LineElement, - PointElement, - Title, - Tooltip, -} from 'chart.js'; -import zoomPlugin from 'chartjs-plugin-zoom'; -import { - ChevronLeft, - ChevronRight, - Expand, - MoveHorizontal, - SkipBack, - SkipForward, - ZoomIn, - ZoomOut, -} from 'lucide-react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Line } from 'react-chartjs-2'; -import { fetchChartData } from '../api'; -import { formatDate, stringToColor } from '../utils'; - -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - zoomPlugin -); - -// Custom tooltip positioner - 50px from cursor, 50px above nearest point -Tooltip.positioners.topCorner = function(elements, eventPosition) { - const chart = this.chart; - const chartCenter = (chart.chartArea.left + chart.chartArea.right) / 2; - const chartVerticalCenter = (chart.chartArea.top + chart.chartArea.bottom) / 2; - const isOnRightSide = eventPosition.x > chartCenter; - const isOnTopSide = eventPosition.y < chartVerticalCenter; - - let x = isOnRightSide ? eventPosition.x - 150 : eventPosition.x + 150; - let y = isOnTopSide ? chart.chartArea.top + 100 : chart.chartArea.bottom - 100; - return { - x, - y, - xAlign: isOnRightSide ? 'right' : 'left', - yAlign: isOnTopSide ? 'top' : 'bottom', - }; -}; - -const DEFAULT_RANGE_SIZE = 100; - -export default function ChartContainer({ - groupName, - chartName, - displayName, - unit, - config, - engineFilter, - onFullscreen, -}) { - const [totalCommits, setTotalCommits] = useState(null); - const [chartData, setChartData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - // viewRange stores the requested range: either { last: N } or { startIdx, endIdx } - const [viewRange, setViewRange] = useState({ last: DEFAULT_RANGE_SIZE }); - const chartRef = useRef(null); - const isResettingZoom = useRef(false); - - // Fetch data for the current view range - useEffect(() => { - let cancelled = false; - - async function loadData() { - setLoading(true); - setError(null); - - try { - let options = {}; - if (viewRange.last) { - // Initial load: get last N commits - options = { last: viewRange.last }; - } else if (viewRange.startIdx !== undefined && viewRange.endIdx !== undefined) { - // Navigation: use index-based range - options = { startIdx: viewRange.startIdx, endIdx: viewRange.endIdx }; - } - - const data = await fetchChartData(groupName, chartName, options); - if (!cancelled && data) { - setChartData(data); - if (data.originalLength) { - setTotalCommits(data.originalLength); - } else if (data.commits) { - setTotalCommits(data.commits.length); - } - } - } catch (err) { - if (!cancelled) { - setError(err.message); - } - } finally { - if (!cancelled) { - setLoading(false); - } - } - } - - loadData(); - - return () => { - cancelled = true; - }; - }, [groupName, chartName, viewRange]); - - // Compute display range info from chartData (for rendering only) - const displayRangeInfo = useMemo(() => { - if (!chartData) return { startIdx: 0, endIdx: 0, total: 0, rangeSize: 0 }; - const total = chartData.originalLength || chartData.commits?.length || 0; - const rangeSize = chartData.commits?.length || 0; - const req = chartData.requestedRange || {}; - const startIdx = req.startIndex ?? (total - rangeSize); - const endIdx = req.endIndex ?? (total - 1); - return { startIdx, endIdx, total, rangeSize }; - }, [chartData]); - - // Compute current range from viewRange state (for navigation calculations) - const getCurrentRange = useCallback(() => { - const total = totalCommits || 0; - if (viewRange.last) { - const rangeSize = Math.min(viewRange.last, total); - return { - startIdx: Math.max(0, total - rangeSize), - endIdx: total - 1, - total, - rangeSize, - }; - } - const startIdx = viewRange.startIdx ?? 0; - const endIdx = viewRange.endIdx ?? (total - 1); - return { - startIdx, - endIdx, - total, - rangeSize: endIdx - startIdx + 1, - }; - }, [viewRange, totalCommits]); - - const isAtStart = displayRangeInfo.startIdx === 0; - const isAtEnd = displayRangeInfo.endIdx >= displayRangeInfo.total - 1; - const currentRangeSize = displayRangeInfo.rangeSize; - - // Navigation handlers - use functional updates to get latest state - const handleGoToStart = useCallback(() => { - const range = getCurrentRange(); - if (range.startIdx === 0 || !range.total) return; - setViewRange({ - startIdx: 0, - endIdx: Math.min(range.rangeSize - 1, range.total - 1), - }); - }, [getCurrentRange]); - - const handleGoToEnd = useCallback(() => { - const range = getCurrentRange(); - if (range.endIdx >= range.total - 1 || !range.total) return; - setViewRange({ last: range.rangeSize }); - }, [getCurrentRange]); - - const handleMoveBackward = useCallback(() => { - const range = getCurrentRange(); - if (range.startIdx === 0 || !range.total) return; - const moveAmount = Math.max(1, Math.floor(range.rangeSize / 2)); - const newStartIdx = Math.max(0, range.startIdx - moveAmount); - const newEndIdx = newStartIdx + range.rangeSize - 1; - setViewRange({ - startIdx: newStartIdx, - endIdx: Math.min(newEndIdx, range.total - 1), - }); - }, [getCurrentRange]); - - const handleMoveForward = useCallback(() => { - const range = getCurrentRange(); - if (range.endIdx >= range.total - 1 || !range.total) return; - const moveAmount = Math.max(1, Math.floor(range.rangeSize / 2)); - const newEndIdx = Math.min(range.total - 1, range.endIdx + moveAmount); - const newStartIdx = Math.max(0, newEndIdx - range.rangeSize + 1); - setViewRange({ startIdx: newStartIdx, endIdx: newEndIdx }); - }, [getCurrentRange]); - - const handleZoomIn = useCallback(() => { - const range = getCurrentRange(); - if (!range.total || range.rangeSize <= 10) return; - const center = Math.floor((range.startIdx + range.endIdx) / 2); - const newRangeSize = Math.max(10, Math.floor(range.rangeSize / 2)); - const halfRange = Math.floor(newRangeSize / 2); - let newStartIdx = center - halfRange; - let newEndIdx = newStartIdx + newRangeSize - 1; - - // Clamp to bounds - if (newStartIdx < 0) { - newStartIdx = 0; - newEndIdx = newRangeSize - 1; - } - if (newEndIdx >= range.total) { - newEndIdx = range.total - 1; - newStartIdx = Math.max(0, newEndIdx - newRangeSize + 1); - } - - setViewRange({ startIdx: newStartIdx, endIdx: newEndIdx }); - }, [getCurrentRange]); - - const handleZoomOut = useCallback(() => { - const range = getCurrentRange(); - if (!range.total) return; - const center = Math.floor((range.startIdx + range.endIdx) / 2); - const newRangeSize = Math.min(range.total, range.rangeSize * 2); - const halfRange = Math.floor(newRangeSize / 2); - let newStartIdx = center - halfRange; - let newEndIdx = newStartIdx + newRangeSize - 1; - - // Clamp to bounds - if (newStartIdx < 0) { - newStartIdx = 0; - newEndIdx = Math.min(newRangeSize - 1, range.total - 1); - } - if (newEndIdx >= range.total) { - newEndIdx = range.total - 1; - newStartIdx = Math.max(0, newEndIdx - newRangeSize + 1); - } - - setViewRange({ startIdx: newStartIdx, endIdx: newEndIdx }); - }, [getCurrentRange]); - - const handleShowFullRange = useCallback(() => { - const range = getCurrentRange(); - if (!range.total) return; - setViewRange({ startIdx: 0, endIdx: range.total - 1 }); - }, [getCurrentRange]); - - const isFullRange = isAtStart && isAtEnd; - - // Handle drag selection zoom - const handleDragZoom = useCallback((startDataIdx, endDataIdx) => { - if (!chartData?.commits || !chartData.requestedRange) return; - - const numCommits = chartData.commits.length; - if (numCommits < 2) return; - - const rangeStart = chartData.requestedRange.startIndex; - const rangeEnd = chartData.requestedRange.endIndex; - const total = chartData.originalLength || rangeEnd + 1; - - // Map chart indices to original dataset indices using linear interpolation - // This correctly handles downsampled data where numCommits < (rangeEnd - rangeStart + 1) - const minIdx = Math.min(startDataIdx, endDataIdx); - const maxIdx = Math.max(startDataIdx, endDataIdx); - const globalStartIdx = rangeStart + Math.round(minIdx / (numCommits - 1) * (rangeEnd - rangeStart)); - const globalEndIdx = rangeStart + Math.round(maxIdx / (numCommits - 1) * (rangeEnd - rangeStart)); - - // Ensure minimum range - if (globalEndIdx - globalStartIdx < 5) return; - - setViewRange({ - startIdx: Math.max(0, globalStartIdx), - endIdx: Math.min(total - 1, globalEndIdx), - }); - }, [chartData]); - - // Process series data with filters and renaming - const processedData = useMemo(() => { - if (!chartData?.series || !chartData?.commits) return null; - - const { series, commits } = chartData; - const datasets = []; - const labels = commits.map(c => formatDate(c.timestamp)); - - Object.entries(series).forEach(([seriesName, points]) => { - // Apply removed datasets filter - if (config.removedDatasets?.has(seriesName)) return; - - // Apply engine filter - if (engineFilter !== 'all') { - const engine = seriesName.split(':')[0].toLowerCase(); - if (engine !== engineFilter && !seriesName.toLowerCase().includes(engineFilter)) { - return; - } - } - - // Rename series if needed - let displaySeriesName = seriesName; - if (config.renamedDatasets) { - const caseInsensitive = {}; - Object.entries(config.renamedDatasets).forEach(([k, v]) => { - caseInsensitive[k.toLowerCase()] = v; - }); - displaySeriesName = caseInsensitive[seriesName.toLowerCase()] || seriesName; - } - - // Check if hidden by default - const hidden = config.hiddenDatasets?.has(seriesName) || - config.hiddenDatasets?.has(displaySeriesName); - - datasets.push({ - label: displaySeriesName, - data: points, - borderColor: stringToColor(displaySeriesName), - backgroundColor: stringToColor(displaySeriesName) + '20', - pointRadius: 2, - pointHoverRadius: 5, - pointStyle: 'cross', - borderWidth: 1.5, - tension: 0, - spanGaps: true, - hidden, - }); - }); - - return { labels, datasets, commits }; - }, [chartData, config, engineFilter]); - - // Handle click on chart point to open commit on GitHub - const handleChartClick = useCallback((event, elements) => { - if (!elements.length || !processedData?.commits) return; - const dataIndex = elements[0].index; - const commit = processedData.commits[dataIndex]; - if (commit?.id) { - window.open(`https://github.com/vortex-data/vortex/commit/${commit.id}`, '_blank'); - } - }, [processedData]); - - // Chart.js options with drag zoom - const options = useMemo(() => ({ - responsive: true, - maintainAspectRatio: false, - animation: false, - onClick: handleChartClick, - onHover: (event, elements) => { - event.native.target.style.cursor = elements.length ? 'pointer' : 'default'; - }, - interaction: { - mode: 'index', - intersect: true, - }, - plugins: { - legend: { - position: 'top', - align: 'start', - labels: { - boxWidth: 12, - padding: 8, - font: { - size: 12, - family: 'Geist, sans-serif', - }, - usePointStyle: true, - pointStyle: 'rectRounded', - }, - }, - tooltip: { - backgroundColor: 'rgba(16, 16, 16, 0.9)', - titleFont: { family: 'Geist, sans-serif', size: 13 }, - bodyFont: { family: 'Geist Mono, monospace', size: 12 }, - padding: 12, - cornerRadius: 4, - position: 'topCorner', - caretSize: 0, - itemSort: (a, b) => b.parsed.y - a.parsed.y, - // Limit to top 10 items by value to prevent tooltip from getting too large - filter: (item, _index, items) => { - if (items.length <= 10) return item.parsed.y != null; - const validItems = items.filter(i => i.parsed.y != null); - if (validItems.length <= 10) return item.parsed.y != null; - const sorted = [...validItems].sort((a, b) => (b.parsed.y ?? 0) - (a.parsed.y ?? 0)); - const top10 = sorted.slice(0, 10); - return top10.some(i => i.datasetIndex === item.datasetIndex); - }, - callbacks: { - title: (items) => { - if (!items.length || !processedData?.commits) return ''; - const commit = processedData.commits[items[0].dataIndex]; - if (!commit) return items[0].label; - const author = commit.author || 'Unknown'; - return `${formatDate(commit.timestamp)} — ${author}\n(${commit.id?.slice(0, 7) || ''}) ${commit.message || ''}`; - }, - label: (item) => { - const value = item.parsed.y; - if (value == null) return null; - const formattedValue = value < 1 ? value.toFixed(4) : value.toFixed(2); - return `${item.dataset.label}: ${formattedValue} ${unit || ''}`; - }, - labelTextColor: (tooltipItem) => { - const chart = tooltipItem.chart; - const activeElements = chart._active || []; - if (activeElements.length === 0) return '#ffffff'; - const lastEvent = chart._lastEvent; - if (!lastEvent) return '#ffffff'; - // Find which dataset point is closest to the cursor - let hoveredDatasetIndex = activeElements[0].datasetIndex; - let closestDist = Infinity; - for (const el of activeElements) { - const point = chart.getDatasetMeta(el.datasetIndex).data[el.index]; - if (point) { - const dist = Math.hypot(point.x - lastEvent.x, point.y - lastEvent.y); - if (dist < closestDist) { - closestDist = dist; - hoveredDatasetIndex = el.datasetIndex; - } - } - } - if (tooltipItem.datasetIndex === hoveredDatasetIndex) { - return '#ffffff'; - } - return 'rgba(255, 255, 255, 0.45)'; - }, - }, - }, - zoom: { - zoom: { - drag: { - enabled: true, - backgroundColor: 'rgba(99, 102, 241, 0.2)', - borderColor: 'rgba(99, 102, 241, 0.8)', - borderWidth: 1, - }, - mode: 'x', - onZoomComplete: ({ chart }) => { - // Prevent infinite loop from resetZoom triggering onZoomComplete - if (isResettingZoom.current) { - isResettingZoom.current = false; - return; - } - - const { min, max } = chart.scales.x; - const startIdx = Math.floor(min); - const endIdx = Math.ceil(max); - if (startIdx >= 0 && endIdx > startIdx) { - handleDragZoom(startIdx, endIdx); - } - // Reset chart zoom state - isResettingZoom.current = true; - chart.resetZoom(); - }, - }, - }, - }, - scales: { - x: { - display: true, - grid: { - display: true, - color: 'rgba(166, 166, 166, 0.12)', - }, - ticks: { - maxRotation: 45, - minRotation: 45, - font: { - size: 11, - family: 'Geist, sans-serif', - }, - maxTicksLimit: 10, - callback: function(value, index, ticks) { - // Always show first and last tick - if (index === 0 || index === ticks.length - 1) { - return this.getLabelForValue(value); - } - // Show intermediate ticks based on maxTicksLimit - const step = Math.ceil(ticks.length / 10); - if (index % step === 0) { - return this.getLabelForValue(value); - } - return null; - }, - }, - }, - y: { - display: true, - beginAtZero: true, - grid: { - color: 'rgba(166, 166, 166, 0.12)', - }, - ticks: { - font: { - size: 12, - family: 'Geist Mono, monospace', - }, - }, - title: { - display: !!unit, - text: unit || '', - font: { - size: 12, - family: 'Geist, sans-serif', - }, - }, - }, - }, - }), [unit, processedData, handleDragZoom, handleChartClick]); - - // Fullscreen handler - const handleFullscreen = useCallback(() => { - if (processedData) { - onFullscreen({ - title: displayName, - groupName, - chartName, - unit, - config, - initialData: processedData, - totalCommits, - currentRange: getCurrentRange(), - }); - } - }, [processedData, displayName, groupName, chartName, unit, config, totalCommits, getCurrentRange, onFullscreen]); - - // Show placeholder only on initial load (no data yet) - const showPlaceholder = !processedData && (loading || error); - const showOverlay = loading && processedData; - - if (showPlaceholder) { - return ( -
-
- {displayName} -
-
- {error ? ( -

Error loading chart

- ) : ( -
- )} -
-
- ); - } - - if (!loading && error) { - return ( -
-
- {displayName} -
-
-

Error loading chart

-
-
- ); - } - - if (!processedData || processedData.datasets.length === 0) { - return ( -
-
- {displayName} -
-
-

No data available

-
-
- ); - } - - return ( -
-
- - {displayName} - {chartData?.downsampleLevel && chartData.downsampleLevel !== '1x' && ( - - {chartData.downsampleLevel} downsampled - - )} - -
-
- - - - - - - -
- -
-
-
- - {showOverlay && ( -
-
-
- )} -
-
- ); -} diff --git a/benchmarks-website/src/components/Header.jsx b/benchmarks-website/src/components/Header.jsx deleted file mode 100644 index 95edda98560..00000000000 --- a/benchmarks-website/src/components/Header.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; - -export default function Header({ - sidebarOpen, - onMenuToggle, - categoryFilter, - onCategoryChange, - searchFilter, - onSearchChange, - viewMode, - onViewModeChange, - onExpandAll, - onCollapseAll, -}) { - return ( -
-
-
- - - - - - Vortex - - -
- -

Vortex Benchmarks

- -
-
- - - - onSearchChange(e.target.value)} - /> -
-
- -
-
- - -
- - - - - GitHub - -
-
-
- ); -} diff --git a/benchmarks-website/src/components/Modal.jsx b/benchmarks-website/src/components/Modal.jsx deleted file mode 100644 index b9bb7d419b1..00000000000 --- a/benchmarks-website/src/components/Modal.jsx +++ /dev/null @@ -1,476 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { Line } from 'react-chartjs-2'; -import { - SkipBack, - ChevronLeft, - ChevronRight, - SkipForward, - ZoomIn, - ZoomOut, - MoveHorizontal, - X, -} from 'lucide-react'; -import { fetchChartData } from '../api'; -import { stringToColor, formatDate } from '../utils'; - -export default function Modal({ chartData, onClose }) { - const [loading, setLoading] = useState(false); - const [viewRange, setViewRange] = useState(null); - const [currentData, setCurrentData] = useState(null); - const [totalCommits, setTotalCommits] = useState(chartData?.totalCommits || 0); - const chartRef = useRef(null); - const isResettingZoom = useRef(false); - - // Initialize with the data passed from parent - useEffect(() => { - if (chartData?.initialData) { - setCurrentData(chartData.initialData); - setTotalCommits(chartData.totalCommits || chartData.initialData.commits?.length || 0); - if (chartData.currentRange) { - setViewRange({ - startIdx: chartData.currentRange.startIdx, - endIdx: chartData.currentRange.endIdx, - }); - } - } - }, [chartData]); - - // Close on escape key - useEffect(() => { - const handleKeyDown = (e) => { - if (e.key === 'Escape') { - onClose(); - } - }; - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [onClose]); - - // Prevent body scroll when modal is open - useEffect(() => { - document.body.style.overflow = 'hidden'; - return () => { - document.body.style.overflow = ''; - }; - }, []); - - const handleBackdropClick = useCallback((e) => { - if (e.target === e.currentTarget) { - onClose(); - } - }, [onClose]); - - // Fetch data for a new range - const fetchRange = useCallback(async (range) => { - if (!chartData?.groupName || !chartData?.chartName) return; - - setLoading(true); - try { - let options = {}; - if (range.last) { - options = { last: range.last }; - } else if (range.startIdx !== undefined && range.endIdx !== undefined) { - options = { startIdx: range.startIdx, endIdx: range.endIdx }; - } - - const data = await fetchChartData(chartData.groupName, chartData.chartName, options); - if (data) { - // Process the data similar to ChartContainer - const processedData = processChartData(data, chartData.config); - setCurrentData(processedData); - if (data.originalLength) { - setTotalCommits(data.originalLength); - } - } - } catch (err) { - console.error('Error fetching range:', err); - } finally { - setLoading(false); - } - }, [chartData]); - - // Process chart data (similar to ChartContainer) - const processChartData = useCallback((data, config) => { - if (!data?.series || !data?.commits) return null; - - const { series, commits } = data; - const datasets = []; - const labels = commits.map(c => formatDate(c.timestamp)); - - Object.entries(series).forEach(([seriesName, points]) => { - if (config?.removedDatasets?.has(seriesName)) return; - - let displaySeriesName = seriesName; - if (config?.renamedDatasets) { - const caseInsensitive = {}; - Object.entries(config.renamedDatasets).forEach(([k, v]) => { - caseInsensitive[k.toLowerCase()] = v; - }); - displaySeriesName = caseInsensitive[seriesName.toLowerCase()] || seriesName; - } - - const dataPoints = points.map(p => p); - const hidden = config?.hiddenDatasets?.has(seriesName) || - config?.hiddenDatasets?.has(displaySeriesName); - - datasets.push({ - label: displaySeriesName, - data: dataPoints, - borderColor: stringToColor(displaySeriesName), - backgroundColor: stringToColor(displaySeriesName) + '20', - pointRadius: 2, - pointHoverRadius: 5, - pointStyle: 'cross', - borderWidth: 1.5, - tension: 0, - spanGaps: true, - hidden, - }); - }); - - return { labels, datasets, commits }; - }, []); - - // Get current range info - const getCurrentRange = useCallback(() => { - if (!viewRange) { - return { startIdx: 0, endIdx: totalCommits - 1, total: totalCommits, rangeSize: totalCommits }; - } - const startIdx = viewRange.startIdx ?? 0; - const endIdx = viewRange.endIdx ?? (totalCommits - 1); - return { - startIdx, - endIdx, - total: totalCommits, - rangeSize: endIdx - startIdx + 1, - }; - }, [viewRange, totalCommits]); - - const range = getCurrentRange(); - const isAtStart = range.startIdx === 0; - const isAtEnd = range.endIdx >= range.total - 1; - const currentRangeSize = range.rangeSize; - const isFullRange = isAtStart && isAtEnd; - - // Navigation handlers - const handleGoToStart = useCallback(() => { - if (isAtStart || !range.total) return; - const newRange = { - startIdx: 0, - endIdx: Math.min(currentRangeSize - 1, range.total - 1), - }; - setViewRange(newRange); - fetchRange(newRange); - }, [isAtStart, currentRangeSize, range.total, fetchRange]); - - const handleGoToEnd = useCallback(() => { - if (isAtEnd || !range.total) return; - const newRange = { last: currentRangeSize }; - setViewRange({ - startIdx: range.total - currentRangeSize, - endIdx: range.total - 1, - }); - fetchRange(newRange); - }, [isAtEnd, currentRangeSize, range.total, fetchRange]); - - const handleMoveBackward = useCallback(() => { - if (isAtStart || !range.total) return; - const moveAmount = Math.max(1, Math.floor(currentRangeSize / 2)); - const newStartIdx = Math.max(0, range.startIdx - moveAmount); - const newEndIdx = newStartIdx + currentRangeSize - 1; - const newRange = { - startIdx: newStartIdx, - endIdx: Math.min(newEndIdx, range.total - 1), - }; - setViewRange(newRange); - fetchRange(newRange); - }, [isAtStart, range, currentRangeSize, fetchRange]); - - const handleMoveForward = useCallback(() => { - if (isAtEnd || !range.total) return; - const moveAmount = Math.max(1, Math.floor(currentRangeSize / 2)); - const newEndIdx = Math.min(range.total - 1, range.endIdx + moveAmount); - const newStartIdx = Math.max(0, newEndIdx - currentRangeSize + 1); - const newRange = { startIdx: newStartIdx, endIdx: newEndIdx }; - setViewRange(newRange); - fetchRange(newRange); - }, [isAtEnd, range, currentRangeSize, fetchRange]); - - const handleZoomIn = useCallback(() => { - if (!range.total || currentRangeSize <= 10) return; - const center = Math.floor((range.startIdx + range.endIdx) / 2); - const newRangeSize = Math.max(10, Math.floor(currentRangeSize / 2)); - const halfRange = Math.floor(newRangeSize / 2); - let newStartIdx = center - halfRange; - let newEndIdx = newStartIdx + newRangeSize - 1; - - if (newStartIdx < 0) { - newStartIdx = 0; - newEndIdx = newRangeSize - 1; - } - if (newEndIdx >= range.total) { - newEndIdx = range.total - 1; - newStartIdx = Math.max(0, newEndIdx - newRangeSize + 1); - } - - const newRange = { startIdx: newStartIdx, endIdx: newEndIdx }; - setViewRange(newRange); - fetchRange(newRange); - }, [range, currentRangeSize, fetchRange]); - - const handleZoomOut = useCallback(() => { - if (!range.total) return; - const center = Math.floor((range.startIdx + range.endIdx) / 2); - const newRangeSize = Math.min(range.total, currentRangeSize * 2); - const halfRange = Math.floor(newRangeSize / 2); - let newStartIdx = center - halfRange; - let newEndIdx = newStartIdx + newRangeSize - 1; - - if (newStartIdx < 0) { - newStartIdx = 0; - newEndIdx = Math.min(newRangeSize - 1, range.total - 1); - } - if (newEndIdx >= range.total) { - newEndIdx = range.total - 1; - newStartIdx = Math.max(0, newEndIdx - newRangeSize + 1); - } - - const newRange = { startIdx: newStartIdx, endIdx: newEndIdx }; - setViewRange(newRange); - fetchRange(newRange); - }, [range, currentRangeSize, fetchRange]); - - const handleShowFullRange = useCallback(() => { - if (!range.total) return; - const newRange = { startIdx: 0, endIdx: range.total - 1 }; - setViewRange(newRange); - fetchRange(newRange); - }, [range.total, fetchRange]); - - // Handle drag selection zoom - const handleDragZoom = useCallback((startDataIdx, endDataIdx) => { - if (!currentData?.commits) return; - - const numCommits = currentData.commits.length; - if (numCommits < 2) return; - - const rangeStart = range.startIdx; - const rangeEnd = range.endIdx; - const total = range.total; - - const minIdx = Math.min(startDataIdx, endDataIdx); - const maxIdx = Math.max(startDataIdx, endDataIdx); - const globalStartIdx = rangeStart + Math.round(minIdx / (numCommits - 1) * (rangeEnd - rangeStart)); - const globalEndIdx = rangeStart + Math.round(maxIdx / (numCommits - 1) * (rangeEnd - rangeStart)); - - if (globalEndIdx - globalStartIdx < 5) return; - - const newRange = { - startIdx: Math.max(0, globalStartIdx), - endIdx: Math.min(total - 1, globalEndIdx), - }; - setViewRange(newRange); - fetchRange(newRange); - }, [currentData, range, fetchRange]); - - // Chart options - const options = useMemo(() => ({ - responsive: true, - maintainAspectRatio: false, - animation: false, - interaction: { - mode: 'index', - intersect: false, - }, - plugins: { - legend: { - position: 'top', - align: 'start', - labels: { - boxWidth: 12, - padding: 8, - font: { size: 11, family: 'Geist, sans-serif' }, - usePointStyle: true, - pointStyle: 'rectRounded', - }, - }, - tooltip: { - backgroundColor: 'rgba(16, 16, 16, 0.9)', - titleFont: { family: 'Geist, sans-serif', size: 12 }, - bodyFont: { family: 'Geist Mono, monospace', size: 11 }, - padding: 12, - cornerRadius: 4, - caretSize: 0, - position: 'topCorner', - itemSort: (a, b) => b.parsed.y - a.parsed.y, - callbacks: { - title: (items) => { - if (!items.length || !currentData?.commits) return ''; - const commit = currentData.commits[items[0].dataIndex]; - if (!commit) return items[0].label; - const author = commit.author || 'Unknown'; - return `${formatDate(commit.timestamp)} — ${author}\n(${commit.id?.slice(0, 7) || ''}) ${commit.message || ''}`; - }, - label: (item) => { - const value = item.parsed.y; - if (value == null) return null; - const formattedValue = value < 1 ? value.toFixed(4) : value.toFixed(2); - return `${item.dataset.label}: ${formattedValue} ${chartData?.unit || ''}`; - }, - }, - }, - zoom: { - zoom: { - drag: { - enabled: true, - backgroundColor: 'rgba(99, 102, 241, 0.2)', - borderColor: 'rgba(99, 102, 241, 0.8)', - borderWidth: 1, - }, - mode: 'x', - onZoomComplete: ({ chart }) => { - if (isResettingZoom.current) { - isResettingZoom.current = false; - return; - } - const { min, max } = chart.scales.x; - const startIdx = Math.floor(min); - const endIdx = Math.ceil(max); - if (startIdx >= 0 && endIdx > startIdx) { - handleDragZoom(startIdx, endIdx); - } - isResettingZoom.current = true; - chart.resetZoom(); - }, - }, - }, - }, - scales: { - x: { - display: true, - grid: { display: true, color: 'rgba(0, 0, 0, 0.12)' }, - ticks: { - maxRotation: 45, - minRotation: 45, - font: { size: 10, family: 'Geist, sans-serif' }, - maxTicksLimit: 15, - callback: function(value, index, ticks) { - // Always show first and last tick - if (index === 0 || index === ticks.length - 1) { - return this.getLabelForValue(value); - } - // Show intermediate ticks based on maxTicksLimit - const step = Math.ceil(ticks.length / 15); - if (index % step === 0) { - return this.getLabelForValue(value); - } - return null; - }, - }, - }, - y: { - display: true, - beginAtZero: true, - grid: { color: 'rgba(0, 0, 0, 0.12)' }, - ticks: { font: { size: 11, family: 'Geist Mono, monospace' } }, - title: { - display: !!chartData?.unit, - text: chartData?.unit || '', - font: { size: 11, family: 'Geist, sans-serif' }, - }, - }, - }, - }), [currentData, chartData, handleDragZoom]); - - if (!chartData) return null; - - return ( -
-
-
-

{chartData.title}

-
-
- - - - - - - -
- -
-
-
- {currentData && ( - - )} - {loading && ( -
-
-
- )} -
-
-
- ); -} diff --git a/benchmarks-website/src/components/Sidebar.jsx b/benchmarks-website/src/components/Sidebar.jsx deleted file mode 100644 index 3a53b801f8e..00000000000 --- a/benchmarks-website/src/components/Sidebar.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -export default function Sidebar({ - isOpen, - groups, - onClose, - onGroupClick, - onClearFilter, - showClearFilter, -}) { - return ( - - ); -} diff --git a/benchmarks-website/src/config.js b/benchmarks-website/src/config.js deleted file mode 100644 index c6ce9060428..00000000000 --- a/benchmarks-website/src/config.js +++ /dev/null @@ -1,276 +0,0 @@ -// ============================================================================= -// SQL query benchmark suites — single source of truth. -// To add a new SQL query benchmark, add one entry to QUERY_SUITES. -// The server (routing/formatting) and frontend (UI config) both derive from this. -// ============================================================================= - -export const QUERY_SUITES = [ - { - prefix: "clickbench", - displayName: "Clickbench", - queryPrefix: "CLICKBENCH", - description: - "ClickHouse's analytical benchmark suite testing real-world query patterns on web analytics data", - tags: ["Queries (NVMe)"], - hiddenDatasets: ["datafusion:lance"], - }, - { - prefix: "statpopgen", - displayName: "Statistical and Population Genetics", - queryPrefix: "STATPOPGEN", - description: - "A suite of Statistical and Population genetics queries using the gnomAD dataset", - tags: ["Queries (NVMe)", "StatPopGen"], - }, - { - prefix: "polarsignals", - displayName: "PolarSignals Profiling", - queryPrefix: "POLARSIGNALS", - description: - "Profiling data benchmark modeled on PolarSignals/Parca, exercising scan-layer performance with projection and filter pushdown on deeply nested schemas", - tags: ["Queries (NVMe)", "PolarSignals"], - }, - { - prefix: "tpch", - displayName: "TPC-H", - queryPrefix: "TPC-H", - datasetKey: "tpch", - fanOut: true, - hiddenDatasets: ["datafusion:lance"], - }, - { - prefix: "tpcds", - displayName: "TPC-DS", - queryPrefix: "TPC-DS", - datasetKey: "tpcds", - fanOut: true, - }, - { prefix: "fineweb", skip: true }, -]; - -// Pre-registered fan-out groups (storage x scale factor). -export const FAN_OUT_GROUPS = [ - "TPC-H (NVMe) (SF=1)", - "TPC-H (S3) (SF=1)", - "TPC-H (NVMe) (SF=10)", - "TPC-H (S3) (SF=10)", - "TPC-H (NVMe) (SF=100)", - "TPC-H (S3) (SF=100)", - "TPC-H (NVMe) (SF=1000)", - "TPC-H (S3) (SF=1000)", - "TPC-DS (NVMe) (SF=1)", - "TPC-DS (NVMe) (SF=10)", -]; - -// Canonical engine:format renaming used by all query suites. -export const ENGINE_RENAMES = { - "datafusion:vortex-file-compressed": "datafusion:vortex", - "datafusion:parquet": "datafusion:parquet", - "datafusion:arrow": "datafusion:in-memory-arrow", - "datafusion:lance": "datafusion:lance", - "datafusion:vortex-compact": "datafusion:vortex-compact", - "duckdb:vortex-file-compressed": "duckdb:vortex", - "duckdb:parquet": "duckdb:parquet", - "duckdb:duckdb": "duckdb:duckdb", - "duckdb:vortex-compact": "duckdb:vortex-compact", - "vortex-tokio-local-disk": "vortex-nvme", - "vortex-compact-tokio-local-disk": "vortex-compact-nvme", - "lance-tokio-local-disk": "lance-nvme", - "parquet-tokio-local-disk": "parquet-nvme", - lance: "lance", -}; - -// ============================================================================= -// Below: frontend UI config, derived from QUERY_SUITES where possible. -// ============================================================================= - -// Build BENCHMARK_CONFIGS: bespoke non-query groups + generated query group entries. -const BESPOKE_CONFIGS = [ - { - name: "Random Access", - renamedDatasets: { - "vortex-tokio-local-disk": "vortex-nvme", - "vortex-compact-tokio-local-disk": "vortex-compact-nvme", - "lance-tokio-local-disk": "lance-nvme", - "parquet-tokio-local-disk": "parquet-nvme", - }, - }, - { - name: "Compression", - keptCharts: [ - "COMPRESS TIME", - "DECOMPRESS TIME", - "PARQUET RS ZSTD COMPRESS TIME", - "PARQUET RS ZSTD DECOMPRESS TIME", - "LANCE COMPRESS TIME", - "LANCE DECOMPRESS TIME", - "VORTEX:PARQUET ZSTD RATIO COMPRESS TIME", - "VORTEX:PARQUET ZSTD RATIO DECOMPRESS TIME", - "VORTEX:LANCE RATIO COMPRESS TIME", - "VORTEX:LANCE RATIO DECOMPRESS TIME", - ], - hiddenDatasets: new Set([ - "wide table cols=1000 chunks=1 rows=1000", - "wide table cols=1000 chunks=50 rows=1000", - ]), - removedDatasets: new Set([ - "TPC-H l_comment canonical", - "TPC-H l_comment chunked without fsst", - "wide table cols=10 chunks=1 rows=1000", - "wide table cols=100 chunks=1 rows=1000", - "wide table cols=10 chunks=50 rows=1000", - "wide table cols=100 chunks=50 rows=1000", - ]), - renamedDatasets: { lance: "lance", Lance: "lance", LANCE: "lance" }, - }, - { - name: "Compression Size", - keptCharts: [ - "VORTEX SIZE", - "PARQUET SIZE", - "LANCE SIZE", - "VORTEX:PARQUET ZSTD SIZE", - "VORTEX:LANCE SIZE", - ], - hiddenDatasets: new Set(["wide table cols=1000"]), - removedDatasets: new Set([ - "wide table cols=10 chunks=1 rows=1000", - "wide table cols=100 chunks=1 rows=1000", - "wide table cols=10 chunks=50 rows=1000", - "wide table cols=100 chunks=50 rows=1000", - ]), - renamedDatasets: { lance: "lance", Lance: "lance", LANCE: "lance" }, - }, -]; - -function querySuiteConfig(name, suite) { - const cfg = { name, renamedDatasets: { ...ENGINE_RENAMES } }; - if (suite?.hiddenDatasets?.length) - cfg.hiddenDatasets = new Set(suite.hiddenDatasets); - return cfg; -} - -function buildQueryConfigs() { - const configs = []; - for (const s of QUERY_SUITES) { - if (s.skip) continue; - if (!s.fanOut) { - configs.push(querySuiteConfig(s.displayName, s)); - } - } - for (const g of FAN_OUT_GROUPS) { - const suite = QUERY_SUITES.find( - (s) => s.fanOut && g.startsWith(s.displayName), - ); - const cfg = querySuiteConfig(g, suite); - if (g.includes("SF=1000") || (g.includes("TPC-DS") && g.includes("SF=10)"))) - cfg.hidden = true; - configs.push(cfg); - } - return configs; -} - -export const BENCHMARK_CONFIGS = [...BESPOKE_CONFIGS, ...buildQueryConfigs()]; - -// Chart name remapping (compression benchmarks only) -export const CHART_NAME_MAP = { - "COMPRESS TIME": "VORTEX WRITE TIME (COMPRESSION)", - "DECOMPRESS TIME": "VORTEX SCAN TIME (DECOMPRESSION)", - "PARQUET RS ZSTD COMPRESS TIME": "PARQUET WRITE TIME (COMPRESSION)", - "PARQUET RS ZSTD DECOMPRESS TIME": "PARQUET SCAN TIME (DECOMPRESSION)", - "LANCE COMPRESS TIME": "LANCE WRITE TIME (COMPRESSION)", - "LANCE DECOMPRESS TIME": "LANCE SCAN TIME (DECOMPRESSION)", - "VORTEX SIZE": "VORTEX SIZE", - "PARQUET ZSTD SIZE": "PARQUET SIZE", - "LANCE SIZE": "LANCE SIZE", - "VORTEX:RAW SIZE": "VORTEX vs RAW SIZE RATIO", - "VORTEX:PARQUET ZSTD SIZE": "VORTEX vs PARQUET SIZE RATIO", - "VORTEX:LANCE SIZE": "VORTEX vs LANCE SIZE RATIO", - "VORTEX:PARQUET ZSTD RATIO COMPRESS TIME": - "VORTEX vs PARQUET WRITE TIME RATIO", - "VORTEX:PARQUET ZSTD RATIO DECOMPRESS TIME": - "VORTEX vs PARQUET SCAN TIME RATIO", - "VORTEX:LANCE RATIO COMPRESS TIME": "VORTEX vs LANCE WRITE TIME RATIO", - "VORTEX:LANCE RATIO DECOMPRESS TIME": "VORTEX vs LANCE SCAN TIME RATIO", -}; - -// Category tags for sidebar filtering -export const CATEGORY_TAGS = { - "Random Access": ["Read/Write"], - Compression: ["Read/Write"], - "Compression Size": ["Read/Write"], -}; -for (const s of QUERY_SUITES) { - if (!s.skip && !s.fanOut && s.tags) CATEGORY_TAGS[s.displayName] = s.tags; -} -for (const g of FAN_OUT_GROUPS) { - const m = g.match(/^(.+?) \((NVMe|S3)\) \((SF=\d+)\)$/); - CATEGORY_TAGS[g] = [ - m[2] === "S3" ? "Queries (S3)" : "Queries (NVMe)", - `${m[1]} (${m[3]})`, - ]; -} - -// Benchmark descriptions -export const BENCHMARK_DESCRIPTIONS = { - "Random Access": - "Tests performance of selecting arbitrary row indices from a file on NVMe storage", - Compression: - "Measures encoding and decoding throughput (MB/s) for Vortex files and Parquet files (with zstd page compression)", - "Compression Size": - "Compares compressed file sizes and compression ratios across different encoding strategies", -}; -for (const s of QUERY_SUITES) { - if (s.description) BENCHMARK_DESCRIPTIONS[s.displayName] = s.description; -} - -// Scale factor descriptions -export const SCALE_FACTOR_DESCRIPTIONS = { - 1: "SF=1 (~1GB of data)", - 10: "SF=10 (~10GB of data)", - 100: "SF=100 (~100GB of data)", - 1000: "SF=1000 (~1TB of data)", -}; - -// Engine filter labels -export const ENGINE_LABELS = { - all: "All", - duckdb: "DuckDB", - datafusion: "DataFusion", - vortex: "Vortex", - parquet: "Parquet", -}; - -// Series color map -export const SERIES_COLOR_MAP = { - "vortex-nvme": "#19a508", - "vortex-compact-nvme": "#15850a", - "parquet-nvme": "#ef7f1d", - "lance-nvme": "#3B82F6", - "datafusion:arrow": "#7a27b1", - "datafusion:in-memory-arrow": "#7a27b1", - "datafusion:parquet": "#ef7f1d", - "datafusion:vortex": "#19a508", - "datafusion:vortex-compact": "#15850a", - "datafusion:lance": "#2D936C", - "duckdb:parquet": "#985113", - "duckdb:vortex": "#0e5e04", - "duckdb:vortex-compact": "#0b4a03", - "duckdb:duckdb": "#87752e", - "vortex:lance": "#FF8787", -}; - -// Fallback color palette -export const FALLBACK_PALETTE = [ - "#5971FD", - "#CEE562", - "#EEB3E1", - "#FF8C42", - "#B8336A", - "#726DA8", - "#2D936C", - "#E9B44C", -]; - -// Default visible commits -export const DEFAULT_COMMIT_RANGE = 100; diff --git a/benchmarks-website/src/main.jsx b/benchmarks-website/src/main.jsx deleted file mode 100644 index 32fcc3f979f..00000000000 --- a/benchmarks-website/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import './styles/index.css'; - -ReactDOM.createRoot(document.getElementById('root')).render( - - - -); diff --git a/benchmarks-website/src/styles/index.css b/benchmarks-website/src/styles/index.css deleted file mode 100644 index b32bf21377c..00000000000 --- a/benchmarks-website/src/styles/index.css +++ /dev/null @@ -1,1319 +0,0 @@ -/* CSS Variables for consistent theming */ -:root { - /* Vortex Brand Colors */ - --vortex-black: #101010; - --vortex-gray: #ECECEC; - --vortex-green: #CEE562; - --vortex-blue: #5971FD; - --vortex-pink: #EEB3E1; - - /* Theme Colors */ - --primary-color: var(--vortex-blue); - --primary-hover: #4A5FE5; - --accent-color: var(--vortex-green); - --bg-color: #ffffff; - --bg-secondary: #FAFAFA; - --text-color: var(--vortex-black); - --text-secondary: #666666; - --border-color: var(--vortex-gray); - - /* Layout */ - --header-height: 72px; - --sidebar-width: 280px; - --chart-spacing: 24px; - --mobile-breakpoint: 768px; - --tablet-breakpoint: 1024px; - - /* Shadows */ - --shadow-sm: 0 1px 3px rgba(16,16,16,0.08); - --shadow-md: 0 4px 8px rgba(16,16,16,0.08); - --shadow-lg: 0 12px 24px rgba(16,16,16,0.12); - - /* Border Radius */ - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; -} - -@media (prefers-color-scheme: dark) { :root { - --primary-color: var(--vortex-blue); - --primary-hover: #8a9cff; - --accent-color: var(--vortex-green); - --bg-color: #050507; - --bg-secondary: #121219; - --text-color: #F5F5F7; - --text-secondary: #A0A0B0; - --border-color: #30303a; - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.6); - --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.7); - --shadow-lg: 0 12px 24px rgba(0, 0, 0, 0.8); -}} - -/* Reset and base styles */ -* { - box-sizing: border-box; -} - -html { - font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", "SF Pro Display", Roboto, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: var(--bg-color); - font-size: 16px; - scroll-behavior: smooth; - overflow-x: hidden; -} - -body { - color: var(--text-color); - margin: 0; - padding: 0; - font-size: 1em; - font-weight: 400; - padding-top: var(--header-height); - line-height: 1.6; - letter-spacing: -0.01em; - overflow-x: hidden; - position: relative; -} - -/* Typography */ -h1, h2, h3, h4, h5, h6 { - font-family: "Funnel Display", sans-serif; - font-weight: 600; - letter-spacing: -0.02em; -} - -code, pre { - font-family: "Geist Mono", monospace; - font-size: 0.9em; -} - -/* Sticky Header */ -.sticky-header { - position: fixed; - top: 0; - left: 0; - right: 0; - height: var(--header-height); - background: rgba(255, 255, 255, 0.95); - -webkit-backdrop-filter: blur(10px); - backdrop-filter: blur(10px); - border-bottom: 1px solid var(--border-color); - box-shadow: var(--shadow-sm); - z-index: 1000; - display: flex; - align-items: center; -} - -.header-content { - display: flex; - align-items: center; - justify-content: space-between; - height: 100%; - padding: 0 32px; - width: 100%; - gap: 24px; - background: var(--bg-secondary); -} - -.header-left { - display: flex; - align-items: center; - gap: 12px; -} - -.menu-toggle { - display: block; - background: none; - border: none; - font-size: 20px; - cursor: pointer; - padding: 8px; - border-radius: var(--radius-sm); - transition: all 0.2s; - color: var(--text-color); -} - -.menu-toggle:hover { - background-color: var(--bg-secondary); -} - -.logo-link { - display: flex; - align-items: center; - text-decoration: none; - transition: opacity 0.2s; -} - -.logo-link:hover { - opacity: 0.8; -} - -.site-logo { - height: 48px; - width: auto; - display: block; -} - -.site-title { - font-family: "Funnel Display", sans-serif; - font-size: 1.5rem; - font-weight: 600; - margin: 0; - margin-left: calc(var(--sidebar-width) - 180px); - color: var(--text-color); - display: none; - white-space: nowrap; -} - -@media (min-width: 1400px) { - .site-title { - display: block; - } -} - -.header-center { - flex: 1; - display: flex; - justify-content: center; - padding: 0 20px; -} - -.filter-controls { - display: flex; - align-items: center; - gap: 16px; - max-width: 600px; -} - -.control-btn, .view-btn { - font-family: "Geist", sans-serif; - padding: 8px 20px; - border: 1px solid var(--border-color); - background: var(--bg-color); - color: var(--text-color); - border-radius: var(--radius-md); - cursor: pointer; - font-size: 14px; - font-weight: 500; - transition: all 0.2s; - white-space: nowrap; -} - -.control-btn:hover, .view-btn:hover { - background-color: var(--bg-secondary); - border-color: var(--primary-color); - transform: translateY(-1px); - box-shadow: var(--shadow-sm); -} - -.view-btn.active { - background-color: var(--primary-color); - color: white; - border-color: var(--primary-color); -} - -.category-filter, .search-filter { - font-family: "Geist", sans-serif; - padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - font-size: 14px; - font-weight: 500; - background: var(--bg-color); - color: var(--text-color); - transition: border-color 0.2s; -} - -.category-filter:focus, .search-filter:focus { - outline: none; - border-color: var(--primary-color); -} - -.search-filter { - width: 200px; -} - -.header-right { - display: flex; - align-items: center; - gap: 16px; -} - -.view-controls { - display: flex; - gap: 4px; -} - -.repo-link { - display: flex; - align-items: center; - gap: 8px; - color: var(--text-color); - text-decoration: none; - font-weight: 600; - font-size: 14px; - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - transition: all 0.2s; -} - -.github-logo { - flex-shrink: 0; -} - -.repo-link:hover { - background-color: var(--bg-secondary); - border-color: var(--primary-color); - color: var(--primary-color); -} - -/* Main Container */ -.main-container { - display: flex; - min-height: calc(100vh - var(--header-height)); - overflow-x: hidden; - width: 100%; -} - -/* Sidebar Navigation */ -.sidebar { - width: var(--sidebar-width); - background: var(--bg-secondary); - -webkit-backdrop-filter: blur(10px); - backdrop-filter: blur(10px); - border-right: 1px solid var(--border-color); - position: fixed; - top: var(--header-height); - left: 0; - height: calc(100vh - var(--header-height)); - overflow-y: auto; - transition: transform 0.3s ease; - z-index: 998; - transform: translateX(-100%); - box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15); -} - -.sidebar.active, -.sidebar.open { - transform: translateX(0); -} - -.sidebar-nav { - display: flex; - flex-direction: column; - height: 100%; -} - -.sidebar-header { - padding: 20px; - border-bottom: 1px solid var(--border-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.sidebar-header h2 { - font-family: "Funnel Display", sans-serif; - margin: 0; - font-size: 1.2rem; - color: var(--text-color); - font-weight: 600; -} - -.sidebar-close { - background: none; - border: none; - font-size: 24px; - cursor: pointer; - padding: 4px; - line-height: 1; -} - -.clear-filter-btn { - font-family: "Geist", sans-serif; - width: calc(100% - 40px); - margin: 16px 20px; - padding: 10px 16px; - background-color: var(--primary-color); - color: white; - border: 1px solid var(--primary-color); - border-radius: var(--radius-md); - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; -} - -.clear-filter-btn:hover { - background-color: var(--primary-hover); - border-color: var(--primary-hover); -} - -.toc-list { - list-style: none; - padding: 0; - margin: 0; - flex: 1; - overflow-y: auto; -} - -.toc-list li { - border-bottom: 1px solid rgba(0,0,0,0.05); -} - -.toc-list a { - display: block; - padding: 12px 20px; - color: var(--text-color); - text-decoration: none; - transition: all 0.2s; - position: relative; -} - -.toc-list a:hover { - background-color: rgba(89, 113, 253, 0.08); - color: var(--primary-color); - padding-left: 24px; -} - -.sidebar-footer { - padding: 20px; - border-top: 1px solid var(--border-color); -} - -.download-btn { - display: block; - width: 100%; - padding: 10px 16px; - background-color: transparent; - color: var(--text-secondary); - text-align: center; - text-decoration: none; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - font-size: 13px; - font-weight: 500; - transition: all 0.2s; -} - -.download-btn:hover { - background-color: var(--bg-secondary); - color: var(--text-color); - border-color: var(--text-secondary); -} - -/* Sidebar overlay */ -.sidebar-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.3); - z-index: 997; -} - -.sidebar-overlay.active { - display: block; -} - -/* Main Content */ -.main-content { - flex: 1; - padding: 32px; - width: 100%; - position: relative; -} - -@media (min-width: 1600px) { - .main-content { - padding: 40px 60px; - } - .header-content { - padding: 0 60px; - } -} - -@media (min-width: 1920px) { - .main-content { - padding: 48px 80px; - } - .header-content { - padding: 0 80px; - } -} - -/* Loading Indicator */ -.loading-indicator, -.error-indicator { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 400px; - gap: 16px; -} - -.loading-indicator p, -.error-indicator p { - font-family: "Geist", sans-serif; - color: var(--text-secondary); - font-size: 14px; - font-weight: 500; -} - -.spinner { - width: 48px; - height: 48px; - border: 3px solid var(--border-color); - border-top: 3px solid var(--primary-color); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Benchmark Sections */ -.benchmark-set { - margin-bottom: 48px; - background: var(--bg-color); - border-radius: var(--radius-lg); - border: 1px solid var(--border-color); - overflow: visible; - transition: opacity 0.3s ease, border-color 0.3s ease; - position: relative; - z-index: 1; -} - -.benchmark-set:first-child { - margin-top: 0; -} - -.benchmark-set.no-data { - opacity: 0.5; -} - -.benchmark-set.no-data .benchmark-header { - cursor: not-allowed; -} - -.benchmark-set.no-data .collapse-icon { - visibility: hidden; -} - -.sticky-header-container { - position: relative; - z-index: 50; - background: var(--bg-secondary); - border-radius: var(--radius-lg) var(--radius-lg) 0 0; - margin: 0; - overflow: visible; - padding: 0; -} - -.benchmark-header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 32px; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - border-radius: var(--radius-lg); - cursor: pointer; - -webkit-user-select: none; - user-select: none; - transition: background-color 0.2s; - overflow: visible; -} - -.benchmark-header:hover { - background: var(--bg-color); -} - -.title-wrapper { - display: flex; - align-items: center; - gap: 12px; - flex: 1; - min-width: 0; - overflow: visible; -} - -.benchmark-title { - font-family: "Funnel Display", sans-serif; - font-size: 1.5rem; - font-weight: 600; - margin: 0; - color: var(--text-color); - display: flex; - align-items: center; - gap: 12px; - letter-spacing: -0.02em; - line-height: 1.2; -} - -.group-link-btn { - font-size: 18px; - background: none; - border: none; - cursor: pointer; - opacity: 0.5; - padding: 4px 8px; - border-radius: var(--radius-sm); - transition: opacity 0.2s, background-color 0.2s, color 0.2s; - color: var(--text-secondary); -} - -.benchmark-header:hover .group-link-btn { - opacity: 1; -} - -.group-link-btn:hover { - background-color: var(--bg-secondary); - color: var(--primary-color); -} - -.group-link-btn.copied { - color: var(--accent-color); - opacity: 1; -} - -.collapse-icon { - font-size: 1rem; - flex-shrink: 0; - width: 1rem; - text-align: center; - display: inline-block; -} - -.benchmark-secondary-info { - display: flex; - align-items: center; - gap: 12px; - flex-shrink: 0; -} - -.benchmark-meta { - font-family: "Geist Mono", monospace; - display: flex; - gap: 16px; - font-size: 11px; - font-weight: 500; - color: var(--text-secondary); - letter-spacing: 0.02em; - text-transform: uppercase; - flex-shrink: 0; - line-height: 1; -} - -.info-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--bg-secondary); - color: var(--text-secondary); - font-size: 16px; - cursor: help; - position: relative; - transition: background-color 0.2s, color 0.2s; - flex-shrink: 0; -} - -.info-icon:hover { - background: var(--primary-color); - color: white; -} - -.info-icon::after { - content: attr(data-tooltip); - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - padding: 8px 12px; - background: rgba(16, 16, 16, 0.95); - color: white; - font-size: 13px; - line-height: 1.4; - border-radius: var(--radius-sm); - white-space: nowrap; - opacity: 0; - visibility: hidden; - transition: opacity 0.2s, visibility 0.2s; - pointer-events: none; - z-index: 10000; - font-weight: normal; -} - -.info-icon:hover::after { - opacity: 1; - visibility: visible; -} - -/* Generic Tooltip */ -[data-tooltip] { - position: relative; -} - -[data-tooltip]::after { - content: attr(data-tooltip); - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - margin-bottom: 6px; - padding: 6px 10px; - background: rgba(16, 16, 16, 0.95); - color: white; - font-family: "Geist", sans-serif; - font-size: 12px; - font-weight: 500; - line-height: 1.3; - border-radius: var(--radius-sm); - white-space: nowrap; - opacity: 0; - visibility: hidden; - transition: all 0.15s; - pointer-events: none; - z-index: 1000; -} - -[data-tooltip]:hover::after { - opacity: 1; - visibility: visible; -} - -[data-tooltip]:disabled::after, -[data-tooltip][disabled]::after { - display: none; -} - -/* Engine Filter Controls */ -.engine-filter-container { - padding: 12px 32px; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; -} - -.engine-filter-label { - font-family: "Geist", sans-serif; - font-size: 14px; - font-weight: 500; - color: var(--text-secondary); -} - -.engine-filter-btn { - font-family: "Geist", sans-serif; - padding: 6px 14px; - border: 1px solid var(--border-color); - background: var(--bg-color); - border-radius: var(--radius-sm); - font-size: 13px; - font-weight: 500; - color: var(--text-color); - cursor: pointer; - transition: all 0.2s; -} - -.engine-filter-btn:hover { - background-color: var(--bg-secondary); - border-color: var(--primary-color); -} - -.engine-filter-btn.active { - background-color: var(--primary-color); - color: white; - border-color: var(--primary-color); -} - -/* Chart Grid */ -.benchmark-graphs { - display: grid; - grid-template-columns: 1fr; - gap: 20px; - padding: 20px; - border-radius: 0 0 var(--radius-lg) var(--radius-lg); - background: var(--bg-color); -} - -@media (min-width: 1200px) { - .benchmark-graphs { - grid-template-columns: repeat(2, 1fr); - gap: 24px; - padding: 24px; - } - - .benchmark-graphs.single-chart { - grid-template-columns: 1fr; - max-width: 1400px; - margin: 0 auto; - } -} - -@media (min-width: 1600px) { - .benchmark-graphs { - padding: 28px 32px; - } -} - -.benchmark-graphs.list-view { - grid-template-columns: 1fr; - max-width: 1200px; - margin: 0 auto; -} - -.chart-container { - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - padding: 20px; - position: relative; - transition: all 0.3s ease; - display: flex; - flex-direction: column; -} - -.chart-container:hover { - box-shadow: var(--shadow-md); -} - -.chart-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.chart-title { - font-family: "Funnel Display", sans-serif; - font-size: 16px; - font-weight: 500; - color: var(--text-color); - letter-spacing: -0.01em; - display: flex; - align-items: center; - gap: 8px; -} - -.downsample-indicator { - font-family: "Geist Mono", monospace; - font-size: 10px; - font-weight: 600; - min-width: 100px; - padding: 2px 6px; - background-color: var(--vortex-pink); - color: var(--vortex-black); - border-radius: var(--radius-sm); - text-transform: uppercase; - letter-spacing: 0.02em; -} - -.chart-actions { - display: flex; - gap: 8px; -} - -.chart-action-btn { - background: var(--bg-color); - border: 1px solid var(--border-color); - padding: 6px 12px; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: 12px; - font-weight: 500; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 6px; -} - -.chart-action-btn:hover { - background-color: #f5f5f5; - border-color: var(--primary-color); -} - -.chart-zoom-controls { - display: flex; - gap: 2px; - margin-right: 8px; -} - -.chart-zoom-btn { - background: var(--bg-color); - border: 1px solid var(--border-color); - padding: 4px 8px; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: 12px; - font-weight: 600; - min-width: 28px; - transition: all 0.15s; - color: var(--text-secondary); - display: flex; - align-items: center; - justify-content: center; -} - -.chart-zoom-btn:hover:not(:disabled) { - background-color: var(--primary-color); - border-color: var(--primary-color); - color: white; -} - -.chart-zoom-btn:disabled { - opacity: 0.4; - cursor: not-allowed; - background: var(--bg-secondary); -} - -/* Chart Canvas */ -.chart-container canvas { - max-height: 450px; - min-height: 320px; - width: 100% !important; - height: auto !important; - display: block; -} - -.benchmark-graphs.list-view .chart-container canvas { - max-height: 600px; - min-height: 400px; -} - -.chart-canvas-wrapper { - position: relative; - height: 100%; - min-height: 320px; -} - -.chart-canvas-wrapper.loading canvas { - filter: blur(2px); - pointer-events: none; - transition: filter 0.2s; -} - -.chart-canvas-placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 320px; - max-height: 450px; - background: var(--bg-secondary); - border-radius: var(--radius-sm); - margin-top: 8px; - position: relative; - overflow: hidden; -} - -.chart-loading-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 10; -} - -.chart-loading-spinner { - width: 32px; - height: 32px; - border: 3px solid var(--border-color); - border-top-color: var(--primary-color); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* Back to Top Button */ -.back-to-top { - position: fixed; - bottom: 32px; - right: 32px; - width: 48px; - height: 48px; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 50%; - font-size: 20px; - cursor: pointer; - box-shadow: 0 4px 12px rgba(89, 113, 253, 0.3); - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; - z-index: 999; -} - -.back-to-top.visible { - opacity: 1; - visibility: visible; -} - -.back-to-top:hover { - background-color: var(--primary-hover); - transform: translateY(-4px); - box-shadow: 0 8px 20px rgba(89, 113, 253, 0.4); -} - -/* Chart Modal */ -.chart-modal { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.8); - z-index: 2000; -} - -.chart-modal.active { - display: flex; - align-items: center; - justify-content: center; -} - -.modal-content { - background: var(--bg-color); - padding: 32px; - border-radius: var(--radius-lg); - width: 95%; - max-width: 1800px; - height: 90vh; - position: relative; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; -} - -.modal-header h2 { - font-family: 'Funnel Display', sans-serif; - font-size: 20px; - font-weight: 600; - margin: 0; -} - -.modal-controls { - display: flex; - align-items: center; - gap: 12px; -} - -.modal-close-btn { - background: var(--bg-color); - border: 1px solid var(--border-color); - padding: 6px; - border-radius: var(--radius-sm); - cursor: pointer; - color: var(--text-secondary); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.15s; -} - -.modal-close-btn:hover { - background-color: var(--primary-color); - border-color: var(--primary-color); - color: white; -} - -.modal-chart-container { - width: 100%; - height: calc(100% - 60px); - position: relative; -} - -.modal-chart-container.loading canvas { - filter: blur(2px); - pointer-events: none; - transition: filter 0.2s; -} - -/* Benchmark Scores Summary */ -.benchmark-scores-summary { - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - padding: 12px 32px; - margin: 0; - margin-top: 0 !important; -} - -.scores-title { - font-size: 12px; - font-weight: 600; - margin: 0 0 8px 0; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.scores-list { - column-count: 2; - column-gap: 20px; -} - -.scores-list:has(.score-item:only-child) { - column-count: 1; -} - -@media (max-width: 780px) { - .scores-list { - column-count: 1; - } -} - -.score-item { - display: flex; - align-items: center; - background: transparent; - padding: 6px 0; - margin-bottom: 4px; - border-radius: 0; - border: none; - transition: none; - font-size: 14px; - break-inside: avoid; -} - -.score-rank { - font-weight: 500; - color: var(--primary-color); - min-width: 24px; - font-size: 14px; -} - -.score-series { - flex: 1; - font-weight: 500; - color: var(--text-color); - margin: 0 8px; - font-size: 14px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.score-metrics { - display: flex; - gap: 6px; - align-items: center; -} - -.score-value { - font-family: 'Geist Mono', monospace; - font-weight: 600; - color: var(--primary-color); - font-size: 13px; -} - -.score-runtime { - font-family: 'Geist Mono', monospace; - font-weight: 600; - color: var(--text-secondary); - background: transparent; - padding: 0 4px; - border-radius: 0; - font-size: 13px; -} - -.scores-explanation { - margin-top: 8px; - font-size: 11px; - color: var(--text-secondary); - font-style: italic; - text-align: center; -} - -/* Utility Classes */ -.hidden { - display: none !important; -} - -/* Mobile Styles */ -@media (max-width: 768px) { - :root { - --header-height: 56px; - } - - .header-content { - padding: 0 8px; - gap: 8px; - } - - .header-left { - gap: 8px; - } - - .site-logo { - height: 32px; - } - - .site-title { - display: none; - } - - .header-center { - display: none; - } - - .view-controls { - display: none; - } - - .repo-link span { - display: none; - } - - .main-content { - padding: 16px; - max-width: 100vw; - } - - .benchmark-set { - margin-bottom: 24px; - border-radius: var(--radius-md); - overflow: hidden; - } - - .benchmark-header { - padding: 12px 16px; - flex-wrap: wrap; - gap: 8px; - overflow: hidden; - } - - .title-wrapper { - flex-wrap: wrap; - gap: 8px; - overflow: hidden; - } - - .benchmark-title { - font-size: 1.1rem; - flex: 1 1 auto; - min-width: 0; - word-break: break-word; - } - - .benchmark-meta { - flex-shrink: 1; - font-size: 10px; - } - - .benchmark-secondary-info { - flex-shrink: 1; - min-width: 0; - } - - .benchmark-graphs { - grid-template-columns: 1fr; - gap: 16px; - padding: 12px; - } - - .chart-container { - padding: 12px; - overflow: hidden; - } - - .chart-header { - flex-wrap: wrap; - gap: 8px; - } - - .chart-title { - flex: 1 1 100%; - min-width: 0; - font-size: 14px; - } - - .chart-actions { - width: 100%; - justify-content: flex-start; - } - - .chart-zoom-controls { - flex-wrap: wrap; - gap: 4px; - } - - .chart-zoom-btn { - padding: 6px 8px; - min-width: 32px; - } - - .chart-container canvas { - max-height: 350px; - min-height: 250px; - } - - .chart-container:hover { - transform: none; - box-shadow: none; - } - - .chart-action-btn { - display: none; - } - - .engine-filter-container { - padding: 12px 16px; - flex-wrap: wrap; - } - - .back-to-top { - bottom: 16px; - right: 16px; - width: 40px; - height: 40px; - font-size: 16px; - } - - .modal-content { - padding: 16px; - height: 90vh; - } - - .modal-header { - flex-wrap: wrap; - gap: 8px; - } - - .modal-header h2 { - font-size: 16px; - min-width: 0; - word-break: break-word; - } - - .benchmark-scores-summary { - padding: 8px 16px; - } -} diff --git a/benchmarks-website/src/utils.js b/benchmarks-website/src/utils.js deleted file mode 100644 index 2d6575fe814..00000000000 --- a/benchmarks-website/src/utils.js +++ /dev/null @@ -1,103 +0,0 @@ -import { SERIES_COLOR_MAP, FALLBACK_PALETTE, CHART_NAME_MAP } from './config'; - -// Simple hash function for color selection -function simpleHash(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - return Math.abs(hash); -} - -export function stringToColor(str) { - if (SERIES_COLOR_MAP[str]) { - return SERIES_COLOR_MAP[str]; - } - - const lowerStr = str - .replace(/^DataFusion:/i, 'datafusion:') - .replace(/^DuckDB:/i, 'duckdb:') - .replace(/^Vortex:/i, 'vortex:') - .replace(/^Arrow:/i, 'arrow:'); - - if (lowerStr !== str && SERIES_COLOR_MAP[lowerStr]) { - return SERIES_COLOR_MAP[lowerStr]; - } - - const index = simpleHash(str) % FALLBACK_PALETTE.length; - return FALLBACK_PALETTE[index]; -} - -export function remapChartName(name) { - if (CHART_NAME_MAP[name]) { - return CHART_NAME_MAP[name]; - } - // Convert dashes to spaces for readability - return name.replace(/-/g, ' '); -} - -export function formatDate(timestamp) { - if (!timestamp) return ''; - const date = new Date(timestamp); - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`; -} - -export function formatTime(ms) { - if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`; - if (ms < 1000) return `${ms.toFixed(1)}ms`; - if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; - return `${(ms / 60000).toFixed(1)}m`; -} - -export function debounce(func, wait) { - let timeout; - return function (...args) { - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(this, args), wait); - }; -} - -export function throttle(func, limit) { - let inThrottle; - return function (...args) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => (inThrottle = false), limit); - } - }; -} - -export function isMobile() { - return window.innerWidth <= 768; -} - -export function getBenchmarkDescription(categoryName) { - if (categoryName.startsWith('TPC-H')) { - const match = categoryName.match(/SF=(\d+)/); - const sf = match ? match[1] : null; - const sfDesc = sf ? `at SF=${sf} (~${sf === '1' ? '1GB' : sf === '10' ? '10GB' : sf === '100' ? '100GB' : '1TB'} of data)` : ''; - if (categoryName.includes('NVMe')) { - return `TPC-H benchmark queries on local NVMe storage ${sfDesc}`; - } else if (categoryName.includes('S3')) { - return `TPC-H benchmark queries against S3 storage ${sfDesc}`; - } - } - if (categoryName.startsWith('TPC-DS')) { - const match = categoryName.match(/SF=(\d+)/); - const sf = match ? match[1] : null; - const sfDesc = sf ? `at SF=${sf}` : ''; - return `TPC-DS benchmark queries on local NVMe storage ${sfDesc}`; - } - const descriptions = { - 'Random Access': 'Tests performance of selecting arbitrary row indices from a file on NVMe storage', - 'Compression': 'Measures encoding and decoding throughput (MB/s) for Vortex and Parquet files', - 'Compression Size': 'Compares compressed file sizes across different encoding strategies', - 'Clickbench': "ClickHouse's analytical benchmark suite on web analytics data", - 'Statistical and Population Genetics': 'Statistical and population genetics queries on gnomAD dataset', - }; - return descriptions[categoryName] || ''; -} diff --git a/benchmarks-website/vite.config.js b/benchmarks-website/vite.config.js deleted file mode 100644 index ad0cc7446f6..00000000000 --- a/benchmarks-website/vite.config.js +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [react()], - publicDir: 'public', - server: { - port: 5173, - proxy: { - '/api': { - target: 'http://localhost:3000', - changeOrigin: true, - }, - }, - }, - build: { - outDir: 'dist', - }, -});