diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e50e09f..e150234b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ ## Unreleased +### Fixed (PR #313 follow-up) + +- The adaptive parallel-policy experiment follow-ups so benchmark/report rows + now describe the plan that actually executed, adaptive planning reuses the + runtime shard-partitioning path instead of duplicating profiling logic, stale + adaptive Criterion directories are selected deterministically, malformed + adaptive benchmark directory names now fail loudly instead of disappearing + from baked reports, conflicting truthful adaptive rows are rejected before + export, and the experimental selector seam stays out of the public + `warp-core` kernel surface while the benchmark-facing adaptive routing + entrypoints remain concrete and deterministic. + ### Fixed (PR #312 follow-up) - The docs-surface reduction follow-ups so the collision tour no longer points diff --git a/crates/warp-benches/benches/README.md b/crates/warp-benches/benches/README.md index fb073a81..9c6c98e9 100644 --- a/crates/warp-benches/benches/README.md +++ b/crates/warp-benches/benches/README.md @@ -33,10 +33,16 @@ results. This README summarizes how to run them and read the output. - static round-robin shard assignment + per-worker deltas - static round-robin shard assignment + per-shard deltas - dedicated one-worker-per-shard + one-delta-per-shard + - adaptive shard routing, which selects a fixed policy from workload shape - Each case includes canonical delta merge after parallel execution, so the study reflects full policy cost for the synthetic independent workload. - - The policy matrix runs across loads `100`, `1000`, and `10000`, with worker - counts `1`, `4`, and `8` where the policy uses a worker pool. + - The policy matrix runs across loads `100`, `1000`, and `10000`, with + concrete `1w`, `4w`, and `8w` executions for the fixed dynamic/static + policies. + - Adaptive rows carry the incoming worker hint and the fixed plan the + selector actually chose for that workload/hint pair, so the baked report + stays truthful when the heuristic collapses to `1w` or caps itself at + `4w`. - Throughput “elements” = executed items in the synthetic independent workload. ## Run diff --git a/crates/warp-benches/benches/parallel_baseline.rs b/crates/warp-benches/benches/parallel_baseline.rs index 426f18f1..f9edefe5 100644 --- a/crates/warp-benches/benches/parallel_baseline.rs +++ b/crates/warp-benches/benches/parallel_baseline.rs @@ -25,7 +25,10 @@ use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criteri use std::collections::BTreeMap; use std::num::NonZeroUsize; use std::time::Duration; -use warp_core::parallel::{build_work_units, execute_work_queue, WorkerResult}; +use warp_core::parallel::{ + build_work_units, execute_parallel_with_adaptive_routing, execute_work_queue, + resolve_adaptive_shard_routing, WorkerResult, +}; use warp_core::{ execute_parallel, execute_parallel_with_policy, execute_serial, make_node_id, make_type_id, make_warp_id, AtomPayload, AttachmentKey, AttachmentValue, ExecItem, GraphStore, GraphView, @@ -62,16 +65,23 @@ fn merge_for_commit_path(deltas: Vec) -> Vec { flat.into_iter().map(|(_, op)| op).collect() } +fn make_test_nodes(n: usize) -> Vec { + let mut nodes = Vec::with_capacity(n); + for i in 0..n { + nodes.push(make_node_id(&format!("bench/n{i}"))); + } + + nodes +} + /// Create a test graph with N independent nodes. fn make_test_store(n: usize) -> (GraphStore, Vec) { let node_ty = make_type_id("bench/node"); let mut store = GraphStore::default(); - let mut nodes = Vec::with_capacity(n); + let nodes = make_test_nodes(n); - for i in 0..n { - let id = make_node_id(&format!("bench/n{i}")); + for &id in &nodes { store.insert_node(id, NodeRecord { ty: node_ty }); - nodes.push(id); } (store, nodes) @@ -317,6 +327,12 @@ fn bench_worker_scaling(c: &mut Criterion) { // Policy matrix comparison // ============================================================================= +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PolicyMatrixCase { + Fixed(ParallelExecutionPolicy), + Adaptive, +} + fn policy_label(policy: ParallelExecutionPolicy) -> &'static str { match policy { ParallelExecutionPolicy::DYNAMIC_PER_WORKER => "dynamic_per_worker", @@ -332,6 +348,19 @@ fn worker_hint(workers: usize) -> NonZeroUsize { NonZeroUsize::new(workers.max(1)).map_or(NonZeroUsize::MIN, |w| w) } +fn adaptive_case_label( + hint: NonZeroUsize, + selected_policy: ParallelExecutionPolicy, + selected_workers: NonZeroUsize, +) -> String { + format!( + "adaptive_shard_routing__hint_{}w__selected_{}_{}w", + hint.get(), + policy_label(selected_policy), + selected_workers.get(), + ) +} + /// Compares shard assignment and delta accumulation strategies directly. /// /// This includes canonical delta merge after parallel execution so the @@ -344,41 +373,24 @@ fn bench_policy_matrix(c: &mut Criterion) { .measurement_time(Duration::from_secs(5)) .sample_size(40); - let policies = [ - ParallelExecutionPolicy::DYNAMIC_PER_WORKER, - ParallelExecutionPolicy::DYNAMIC_PER_SHARD, - ParallelExecutionPolicy::STATIC_PER_WORKER, - ParallelExecutionPolicy::STATIC_PER_SHARD, - ParallelExecutionPolicy::DEDICATED_PER_SHARD, + let cases = [ + PolicyMatrixCase::Fixed(ParallelExecutionPolicy::DYNAMIC_PER_WORKER), + PolicyMatrixCase::Fixed(ParallelExecutionPolicy::DYNAMIC_PER_SHARD), + PolicyMatrixCase::Fixed(ParallelExecutionPolicy::STATIC_PER_WORKER), + PolicyMatrixCase::Fixed(ParallelExecutionPolicy::STATIC_PER_SHARD), + PolicyMatrixCase::Fixed(ParallelExecutionPolicy::DEDICATED_PER_SHARD), + PolicyMatrixCase::Adaptive, ]; for &n in &[100usize, 1_000, 10_000] { group.throughput(Throughput::Elements(n as u64)); - for policy in policies { - if policy == ParallelExecutionPolicy::DEDICATED_PER_SHARD { - group.bench_with_input(BenchmarkId::new(policy_label(policy), n), &n, |b, &n| { - b.iter_batched( - || { - let (store, nodes) = make_test_store(n); - let items = make_exec_items(&nodes); - (store, items) - }, - |(store, items)| { - let view = GraphView::new(&store); - let deltas = - execute_parallel_with_policy(view, &items, worker_hint(1), policy); - let merged = merge_for_commit_path(deltas); - criterion::black_box(merged) - }, - BatchSize::SmallInput, - ); - }); - continue; - } - - for &workers in &[1usize, 4, 8] { + for case in cases { + if case == PolicyMatrixCase::Fixed(ParallelExecutionPolicy::DEDICATED_PER_SHARD) { group.bench_with_input( - BenchmarkId::new(format!("{}/{}w", policy_label(policy), workers), n), + BenchmarkId::new( + policy_label(ParallelExecutionPolicy::DEDICATED_PER_SHARD), + n, + ), &n, |b, &n| { b.iter_batched( @@ -392,8 +404,8 @@ fn bench_policy_matrix(c: &mut Criterion) { let deltas = execute_parallel_with_policy( view, &items, - worker_hint(workers), - policy, + worker_hint(1), + ParallelExecutionPolicy::DEDICATED_PER_SHARD, ); let merged = merge_for_commit_path(deltas); criterion::black_box(merged) @@ -402,6 +414,53 @@ fn bench_policy_matrix(c: &mut Criterion) { ); }, ); + continue; + } + + for &workers in &[1usize, 4, 8] { + let benchmark_label = match case { + PolicyMatrixCase::Fixed(policy) => { + format!("{}/{}w", policy_label(policy), workers) + } + PolicyMatrixCase::Adaptive => { + let nodes = make_test_nodes(n); + let items = make_exec_items(&nodes); + let hint = worker_hint(workers); + let (selected_policy, selected_workers) = + resolve_adaptive_shard_routing(&items, hint); + adaptive_case_label(hint, selected_policy, selected_workers) + } + }; + group.bench_with_input(BenchmarkId::new(benchmark_label, n), &n, |b, &n| { + b.iter_batched( + || { + let (store, nodes) = make_test_store(n); + let items = make_exec_items(&nodes); + (store, items) + }, + |(store, items)| { + let view = GraphView::new(&store); + let deltas = match case { + PolicyMatrixCase::Fixed(policy) => execute_parallel_with_policy( + view, + &items, + worker_hint(workers), + policy, + ), + PolicyMatrixCase::Adaptive => { + execute_parallel_with_adaptive_routing( + view, + &items, + worker_hint(workers), + ) + } + }; + let merged = merge_for_commit_path(deltas); + criterion::black_box(merged) + }, + BatchSize::SmallInput, + ); + }); } } } diff --git a/crates/warp-core/src/parallel/exec.rs b/crates/warp-core/src/parallel/exec.rs index bbb81b84..bef638dc 100644 --- a/crates/warp-core/src/parallel/exec.rs +++ b/crates/warp-core/src/parallel/exec.rs @@ -109,6 +109,188 @@ impl ParallelExecutionPolicy { } } +/// Lightweight shape summary of a shard-partitioned parallel workload. +/// +/// This profile is deterministic and cheap to derive once `partition_into_shards()` +/// has already grouped items by shard. Selectors can use it to choose a stable +/// execution plan without inspecting machine-local runtime state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ParallelExecutionWorkloadProfile { + total_items: usize, + non_empty_shards: usize, + max_shard_len: usize, +} + +impl ParallelExecutionWorkloadProfile { + /// Creates a workload profile from aggregate counts. + #[must_use] + const fn new(total_items: usize, non_empty_shards: usize, max_shard_len: usize) -> Self { + Self { + total_items, + non_empty_shards, + max_shard_len, + } + } + + /// Returns the total number of items in the workload. + #[must_use] + const fn total_items(self) -> usize { + self.total_items + } + + /// Returns the number of non-empty virtual shards in the workload. + #[must_use] + const fn non_empty_shards(self) -> usize { + self.non_empty_shards + } + + /// Returns the size of the largest non-empty virtual shard. + #[must_use] + const fn max_shard_len(self) -> usize { + self.max_shard_len + } + + fn from_shards(shards: &[super::shard::VirtualShard]) -> Self { + let mut total_items = 0; + let mut non_empty_shards = 0; + let mut max_shard_len = 0; + + for shard in shards { + let shard_len = shard.items.len(); + total_items += shard_len; + if shard_len > 0 { + non_empty_shards += 1; + max_shard_len = max_shard_len.max(shard_len); + } + } + + Self::new(total_items, non_empty_shards, max_shard_len) + } +} + +/// Resolved plan for one parallel execution attempt. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ParallelExecutionPlan { + workers: NonZeroUsize, + policy: ParallelExecutionPolicy, +} + +impl ParallelExecutionPlan { + /// Creates a new plan from a worker count and fixed execution policy. + #[must_use] + const fn new(workers: NonZeroUsize, policy: ParallelExecutionPolicy) -> Self { + Self { workers, policy } + } + + /// Returns the worker count selected for this execution. + #[must_use] + const fn workers(self) -> NonZeroUsize { + self.workers + } + + /// Returns the fixed execution policy selected for this execution. + #[must_use] + const fn policy(self) -> ParallelExecutionPolicy { + self.policy + } +} + +/// Selects a deterministic parallel execution plan from a worker hint and workload profile. +trait ParallelExecutionPlanSelector { + /// Chooses the fixed execution plan to use for this workload. + fn select_plan( + &self, + worker_hint: NonZeroUsize, + workload: ParallelExecutionWorkloadProfile, + ) -> ParallelExecutionPlan; +} + +/// Workload-aware selector for shard-routing policy experiments. +/// +/// The heuristic is intentionally conservative: +/// - very small or effectively serial workloads collapse to `STATIC_PER_WORKER` on `1w` +/// - medium workloads use `DYNAMIC_PER_WORKER` on `1w` +/// - large, well-distributed workloads switch to `DYNAMIC_PER_SHARD` with up to `4w` +/// +/// This selector is meant for benchmarking and tuning. It does not observe +/// ambient machine state and remains a pure function of workload shape. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct AdaptiveShardRoutingSelector; + +const ADAPTIVE_SMALL_WORKLOAD_ITEMS: usize = 256; +const ADAPTIVE_LARGE_WORKLOAD_ITEMS: usize = 4_096; +const ADAPTIVE_MIN_PARALLEL_SHARDS: usize = 4; +const ADAPTIVE_DYNAMIC_PER_SHARD_WORKERS: usize = 4; + +impl ParallelExecutionPlanSelector for AdaptiveShardRoutingSelector { + fn select_plan( + &self, + worker_hint: NonZeroUsize, + workload: ParallelExecutionWorkloadProfile, + ) -> ParallelExecutionPlan { + let effective_shard_parallelism = workload.non_empty_shards().min(worker_hint.get()).max(1); + + if workload.total_items() <= ADAPTIVE_SMALL_WORKLOAD_ITEMS + || effective_shard_parallelism <= 1 + { + return ParallelExecutionPlan::new( + NonZeroUsize::MIN, + ParallelExecutionPolicy::STATIC_PER_WORKER, + ); + } + + if workload.total_items() >= ADAPTIVE_LARGE_WORKLOAD_ITEMS + && effective_shard_parallelism >= ADAPTIVE_MIN_PARALLEL_SHARDS + && workload.max_shard_len().saturating_mul(2) <= workload.total_items() + { + let workers = non_zero( + worker_hint + .get() + .min(ADAPTIVE_DYNAMIC_PER_SHARD_WORKERS) + .min(workload.non_empty_shards()) + .max(1), + ); + return ParallelExecutionPlan::new(workers, ParallelExecutionPolicy::DYNAMIC_PER_SHARD); + } + + ParallelExecutionPlan::new( + NonZeroUsize::MIN, + ParallelExecutionPolicy::DYNAMIC_PER_WORKER, + ) + } +} + +fn resolve_partitioned_plan( + shards: &[super::shard::VirtualShard], + worker_hint: NonZeroUsize, + selector: S, +) -> (ParallelExecutionWorkloadProfile, ParallelExecutionPlan) +where + S: ParallelExecutionPlanSelector, +{ + let workload = ParallelExecutionWorkloadProfile::from_shards(shards); + let plan = selector.select_plan(worker_hint, workload); + ( + workload, + ParallelExecutionPlan::new(capped_workers(plan.workers()), plan.policy()), + ) +} + +/// Resolves the fixed policy and worker count chosen by the adaptive shard-routing heuristic. +/// +/// This shares the same shard-partitioned profiling path used by the runtime so +/// benches and reports describe the concrete plan that actually executes. +#[must_use] +pub fn resolve_adaptive_shard_routing( + items: &[ExecItem], + worker_hint: NonZeroUsize, +) -> (ParallelExecutionPolicy, NonZeroUsize) { + let worker_hint = capped_workers(worker_hint); + let shards = partition_into_shards(items); + let (_, plan) = resolve_partitioned_plan(&shards, worker_hint, AdaptiveShardRoutingSelector); + (plan.policy(), plan.workers()) +} + impl Default for ParallelExecutionPolicy { fn default() -> Self { Self::DEFAULT @@ -264,8 +446,7 @@ pub fn execute_parallel(view: GraphView<'_>, items: &[ExecItem], workers: usize) assert!(workers >= 1, "need at least one worker"); // Cap workers at NUM_SHARDS - no point spawning 512 threads for 256 shards - let capped_workers = - NonZeroUsize::new(workers.min(NUM_SHARDS)).map_or(NonZeroUsize::MIN, |w| w); + let capped_workers = capped_workers(non_zero(workers)); execute_parallel_sharded_with_policy( view, @@ -301,7 +482,7 @@ pub fn execute_parallel_sharded( workers: usize, ) -> Vec { assert!(workers >= 1, "need at least one worker"); - let workers = NonZeroUsize::new(workers).map_or(NonZeroUsize::MIN, |w| w); + let workers = non_zero(workers); execute_parallel_sharded_with_policy(view, items, workers, ParallelExecutionPolicy::DEFAULT) } @@ -321,43 +502,68 @@ pub fn execute_parallel_sharded_with_policy( workers: NonZeroUsize, policy: ParallelExecutionPolicy, ) -> Vec { - let workers = workers.get(); - - if items.is_empty() { - return match policy.assignment { - ShardAssignmentPolicy::DedicatedPerShard => Vec::new(), - _ => { - // Can't use vec![TickDelta::new(); workers] because TickDelta doesn't impl Clone - (0..workers).map(|_| TickDelta::new()).collect() - } + let workers = capped_workers(workers); + let shards = partition_into_shards(items); + let workload = ParallelExecutionWorkloadProfile::from_shards(&shards); + execute_partitioned_shards( + view, + &shards, + workload, + ParallelExecutionPlan::new(workers, policy), + ) +} + +/// Parallel execution with the adaptive shard-routing heuristic. +pub fn execute_parallel_sharded_with_adaptive_routing( + view: GraphView<'_>, + items: &[ExecItem], + workers: NonZeroUsize, +) -> Vec { + let workers = capped_workers(workers); + let shards = partition_into_shards(items); + let (workload, plan) = resolve_partitioned_plan(&shards, workers, AdaptiveShardRoutingSelector); + execute_partitioned_shards(view, &shards, workload, plan) +} + +fn execute_partitioned_shards( + view: GraphView<'_>, + shards: &[super::shard::VirtualShard], + workload: ParallelExecutionWorkloadProfile, + plan: ParallelExecutionPlan, +) -> Vec { + if workload.total_items() == 0 { + return if plan.policy().assignment == ShardAssignmentPolicy::DedicatedPerShard { + Vec::new() + } else { + let workers = plan.workers().get(); + (0..workers).map(|_| TickDelta::new()).collect() }; } - // Partition into virtual shards by scope - let shards = partition_into_shards(items); - match (policy.assignment, policy.accumulation) { + let workers = plan.workers().get(); + match (plan.policy().assignment(), plan.policy().accumulation()) { (ShardAssignmentPolicy::DynamicSteal, DeltaAccumulationPolicy::PerWorker) => { - execute_dynamic_per_worker(view, &shards, workers) + execute_dynamic_per_worker(view, shards, workers) } (ShardAssignmentPolicy::DynamicSteal, DeltaAccumulationPolicy::PerShard) => { - execute_dynamic_per_shard(view, &shards, workers) + execute_dynamic_per_shard(view, shards, workers) } (ShardAssignmentPolicy::StaticRoundRobin, DeltaAccumulationPolicy::PerWorker) => { - execute_static_per_worker(view, &shards, workers) + execute_static_per_worker(view, shards, workers) } (ShardAssignmentPolicy::StaticRoundRobin, DeltaAccumulationPolicy::PerShard) => { - execute_static_per_shard(view, &shards, workers) + execute_static_per_shard(view, shards, workers) } ( ShardAssignmentPolicy::DedicatedPerShard, DeltaAccumulationPolicy::PerWorker | DeltaAccumulationPolicy::PerShard, ) => { debug_assert_eq!( - policy.accumulation, + plan.policy().accumulation(), DeltaAccumulationPolicy::PerShard, "DedicatedPerShard is only exposed with PerShard accumulation" ); - execute_dedicated_per_shard(view, &shards) + execute_dedicated_per_shard(view, shards) } } } @@ -372,9 +578,31 @@ pub fn execute_parallel_with_policy( workers: NonZeroUsize, policy: ParallelExecutionPolicy, ) -> Vec { - let capped_workers = - NonZeroUsize::new(workers.get().min(NUM_SHARDS)).map_or(NonZeroUsize::MIN, |w| w); - execute_parallel_sharded_with_policy(view, items, capped_workers, policy) + execute_parallel_sharded_with_policy(view, items, capped_workers(workers), policy) +} + +/// Parallel execution entry point with the adaptive shard-routing heuristic. +pub fn execute_parallel_with_adaptive_routing( + view: GraphView<'_>, + items: &[ExecItem], + workers: NonZeroUsize, +) -> Vec { + execute_parallel_sharded_with_adaptive_routing(view, items, capped_workers(workers)) +} + +const fn non_zero(value: usize) -> NonZeroUsize { + match NonZeroUsize::new(value) { + Some(value) => value, + None => NonZeroUsize::MIN, + } +} + +const fn capped_workers(workers: NonZeroUsize) -> NonZeroUsize { + non_zero(if workers.get() < NUM_SHARDS { + workers.get() + } else { + NUM_SHARDS + }) } fn execute_shard_into_delta(view: GraphView<'_>, items: &[ExecItem], delta: &mut TickDelta) { @@ -853,7 +1081,12 @@ fn execute_item_enforced( #[cfg(test)] mod tests { - use super::{execute_parallel_with_policy, ExecItem, ParallelExecutionPolicy}; + use super::{ + execute_parallel_with_adaptive_routing, execute_parallel_with_policy, + resolve_adaptive_shard_routing, AdaptiveShardRoutingSelector, ExecItem, + ParallelExecutionPlan, ParallelExecutionPlanSelector, ParallelExecutionPolicy, + ParallelExecutionWorkloadProfile, + }; use crate::{ execute_serial, make_type_id, merge_deltas_ok, AtomPayload, AttachmentKey, AttachmentValue, GraphStore, GraphView, NodeId, NodeKey, NodeRecord, OpOrigin, TickDelta, WarpOp, @@ -882,10 +1115,12 @@ mod tests { for i in 0..count { let mut bytes = [0u8; 32]; assert!( - u8::try_from(i).is_ok(), - "test fixture only supports up to 256 scopes" + u16::try_from(i).is_ok(), + "test fixture only supports up to 65_536 scopes" ); - bytes[0] = u8::try_from(i).unwrap_or(0); + let index = u16::try_from(i).unwrap_or(0); + bytes[0] = (index & 0x00ff) as u8; + bytes[1] = (index >> 8) as u8; let scope = NodeId(bytes); store.insert_node(scope, NodeRecord { ty: node_ty }); items.push(ExecItem::new( @@ -906,6 +1141,116 @@ mod tests { NonZeroUsize::new(workers.max(1)).map_or(NonZeroUsize::MIN, |w| w) } + fn adaptive_plan( + total_items: usize, + non_empty_shards: usize, + max_shard_len: usize, + workers: usize, + ) -> ParallelExecutionPlan { + AdaptiveShardRoutingSelector.select_plan( + worker_hint(workers), + ParallelExecutionWorkloadProfile::new(total_items, non_empty_shards, max_shard_len), + ) + } + + #[test] + fn adaptive_selector_prefers_static_per_worker_for_small_workloads() { + let selector = AdaptiveShardRoutingSelector; + let plan = selector.select_plan( + worker_hint(8), + ParallelExecutionWorkloadProfile::new(100, 100, 1), + ); + + assert_eq!( + plan, + ParallelExecutionPlan::new(worker_hint(1), ParallelExecutionPolicy::STATIC_PER_WORKER) + ); + } + + #[test] + fn adaptive_selector_prefers_dynamic_per_worker_for_medium_workloads() { + let selector = AdaptiveShardRoutingSelector; + let plan = selector.select_plan( + worker_hint(8), + ParallelExecutionWorkloadProfile::new(1_000, 32, 48), + ); + + assert_eq!( + plan, + ParallelExecutionPlan::new(worker_hint(1), ParallelExecutionPolicy::DYNAMIC_PER_WORKER) + ); + } + + #[test] + fn adaptive_selector_prefers_dynamic_per_shard_for_large_wide_workloads() { + let selector = AdaptiveShardRoutingSelector; + let plan = selector.select_plan( + worker_hint(8), + ParallelExecutionWorkloadProfile::new(10_000, 64, 120), + ); + + assert_eq!( + plan, + ParallelExecutionPlan::new(worker_hint(4), ParallelExecutionPolicy::DYNAMIC_PER_SHARD) + ); + } + + #[test] + fn adaptive_selector_boundary_total_256() { + assert_eq!( + adaptive_plan(256, 8, 32, 8), + ParallelExecutionPlan::new(worker_hint(1), ParallelExecutionPolicy::STATIC_PER_WORKER) + ); + assert_eq!( + adaptive_plan(257, 8, 32, 8), + ParallelExecutionPlan::new(worker_hint(1), ParallelExecutionPolicy::DYNAMIC_PER_WORKER) + ); + } + + #[test] + fn adaptive_selector_boundary_total_4096() { + assert_eq!( + adaptive_plan(4_095, 8, 512, 8), + ParallelExecutionPlan::new(worker_hint(1), ParallelExecutionPolicy::DYNAMIC_PER_WORKER) + ); + assert_eq!( + adaptive_plan(4_096, 8, 512, 8), + ParallelExecutionPlan::new(worker_hint(4), ParallelExecutionPolicy::DYNAMIC_PER_SHARD) + ); + assert_eq!( + adaptive_plan(4_097, 8, 512, 8), + ParallelExecutionPlan::new(worker_hint(4), ParallelExecutionPolicy::DYNAMIC_PER_SHARD) + ); + } + + #[test] + fn adaptive_selector_boundary_non_empty_shards_4() { + assert_eq!( + adaptive_plan(4_096, 3, 1_000, 8), + ParallelExecutionPlan::new(worker_hint(1), ParallelExecutionPolicy::DYNAMIC_PER_WORKER) + ); + assert_eq!( + adaptive_plan(4_096, 4, 1_000, 8), + ParallelExecutionPlan::new(worker_hint(4), ParallelExecutionPolicy::DYNAMIC_PER_SHARD) + ); + } + + #[test] + fn adaptive_selector_boundary_max_shard_len_gate() { + assert_eq!( + adaptive_plan(4_096, 8, 2_047, 8), + ParallelExecutionPlan::new(worker_hint(4), ParallelExecutionPolicy::DYNAMIC_PER_SHARD) + ); + assert_eq!( + adaptive_plan(4_096, 8, 2_048, 8), + ParallelExecutionPlan::new(worker_hint(4), ParallelExecutionPolicy::DYNAMIC_PER_SHARD) + ); + assert_eq!( + adaptive_plan(4_096, 8, 2_049, 8), + ParallelExecutionPlan::new(worker_hint(1), ParallelExecutionPolicy::DYNAMIC_PER_WORKER) + ); + } + #[test] fn all_parallel_policies_match_serial_oracle() { let policies = [ @@ -1081,4 +1426,57 @@ mod tests { "empty workload should produce no deltas for dedicated-per-shard" ); } + + #[test] + fn adaptive_selector_matches_serial_oracle() { + for item_count in [64_usize, 1_024, 10_000] { + let (store, items) = make_store_and_items(item_count); + let view = GraphView::new(&store); + let serial_oracle_result = merge_deltas_ok(vec![execute_serial(view, &items)]); + assert!( + serial_oracle_result.is_ok(), + "serial oracle merge failed for adaptive selector @ {item_count}: {serial_oracle_result:?}" + ); + let Ok(serial_oracle) = serial_oracle_result else { + unreachable!("assert above guarantees a valid serial oracle"); + }; + + for workers in [1_usize, 4, 8] { + let deltas = + execute_parallel_with_adaptive_routing(view, &items, worker_hint(workers)); + let merged_result = merge_deltas_ok(deltas); + assert!( + merged_result.is_ok(), + "adaptive selector merge failed at {workers}w for {item_count} items: {merged_result:?}" + ); + let Ok(merged) = merged_result else { + unreachable!("assert above guarantees a valid adaptive merge"); + }; + assert_eq!( + merged, serial_oracle, + "adaptive selector changed merged ops at {workers}w for {item_count} items" + ); + } + } + } + + #[test] + fn resolve_adaptive_shard_routing_matches_execution_paths() { + let (_, small_items) = make_store_and_items(64); + let (_, medium_items) = make_store_and_items(1_000); + let (_, large_items) = make_store_and_items(10_000); + + assert_eq!( + resolve_adaptive_shard_routing(&small_items, worker_hint(1)), + (ParallelExecutionPolicy::STATIC_PER_WORKER, worker_hint(1),) + ); + assert_eq!( + resolve_adaptive_shard_routing(&medium_items, worker_hint(4)), + (ParallelExecutionPolicy::DYNAMIC_PER_WORKER, worker_hint(1),) + ); + assert_eq!( + resolve_adaptive_shard_routing(&large_items, worker_hint(8)), + (ParallelExecutionPolicy::DYNAMIC_PER_SHARD, worker_hint(4),) + ); + } } diff --git a/crates/warp-core/src/parallel/mod.rs b/crates/warp-core/src/parallel/mod.rs index cc7ceaa3..0366881b 100644 --- a/crates/warp-core/src/parallel/mod.rs +++ b/crates/warp-core/src/parallel/mod.rs @@ -13,9 +13,10 @@ pub mod shard; pub(crate) use exec::ExecItemKind; pub use exec::{ build_work_units, execute_parallel, execute_parallel_sharded, - execute_parallel_sharded_with_policy, execute_parallel_with_policy, execute_serial, - execute_work_queue, DeltaAccumulationPolicy, ExecItem, ParallelExecutionPolicy, PoisonedDelta, - ShardAssignmentPolicy, WorkUnit, WorkerResult, + execute_parallel_sharded_with_adaptive_routing, execute_parallel_sharded_with_policy, + execute_parallel_with_adaptive_routing, execute_parallel_with_policy, execute_serial, + execute_work_queue, resolve_adaptive_shard_routing, DeltaAccumulationPolicy, ExecItem, + ParallelExecutionPolicy, PoisonedDelta, ShardAssignmentPolicy, WorkUnit, WorkerResult, }; #[cfg(not(any(test, feature = "delta_validate")))] pub(crate) use merge::check_write_to_new_warp; diff --git a/docs/benchmarks/PARALLEL_POLICY_MATRIX.md b/docs/benchmarks/PARALLEL_POLICY_MATRIX.md index facc897c..c9fb274a 100644 --- a/docs/benchmarks/PARALLEL_POLICY_MATRIX.md +++ b/docs/benchmarks/PARALLEL_POLICY_MATRIX.md @@ -13,6 +13,7 @@ count: - static round-robin shard assignment + one delta per worker - static round-robin shard assignment + one delta per shard - dedicated one-worker-per-shard + one delta per shard +- adaptive shard routing, which selects one of the pooled-worker policies from workload shape The point is to answer a narrower question than "is parallel good?": @@ -32,7 +33,7 @@ The benchmark currently runs at: - `1000` - `10000` -For pooled-worker policies, it also varies worker counts: +For the fixed dynamic/static policies, it also varies concrete worker counts: - `1` - `4` @@ -41,6 +42,15 @@ For pooled-worker policies, it also varies worker counts: The dedicated per-shard policy intentionally ignores the worker-count knob and spawns one thread per non-empty shard. +For the adaptive selector, the report preserves both: + +- the incoming worker hint used to seed the heuristic, and +- the fixed policy/worker plan the selector actually chose for that + workload/hint pair + +That keeps the baked HTML and JSON honest when multiple hints collapse to the +same concrete plan. + ## Outputs Running the dedicated bake target produces: diff --git a/docs/benchmarks/index.html b/docs/benchmarks/index.html index 747bca71..2a6bd5a7 100644 --- a/docs/benchmarks/index.html +++ b/docs/benchmarks/index.html @@ -849,6 +849,7 @@

Raw policy table

static_per_worker: "#fbbf24", static_per_shard: "#fb7185", dedicated_per_shard: "#c084fc", + adaptive_shard_routing: "#f97316", }; const POLICY_LABELS = { dynamic_per_worker: "Dynamic claim / worker delta", @@ -856,6 +857,7 @@

Raw policy table

static_per_worker: "Static round-robin / worker delta", static_per_shard: "Static round-robin / shard delta", dedicated_per_shard: "Dedicated shard / shard delta", + adaptive_shard_routing: "Adaptive shard routing selector", }; const WORKER_DASH = { "1w": null, @@ -924,8 +926,38 @@

Raw policy table

return `${(ns / 1e6).toFixed(2)} ms`; } + function workerPoolLabel(workers) { + return WORKER_LABELS[workers] ?? workers; + } + + function workerHintLabel(workers) { + const label = workerPoolLabel(workers); + return label.endsWith("pool") + ? label.replace("pool", "hint") + : `${label} hint`; + } + + function selectedPolicy(row) { + return row.selected_policy ?? row.policy; + } + + function selectedWorkers(row) { + return row.selected_workers ?? row.workers; + } + + function selectedPlanLabel(row) { + return `${POLICY_LABELS[selectedPolicy(row)] ?? selectedPolicy(row)} / ${workerPoolLabel(selectedWorkers(row))}`; + } + function caseLabel(row) { - return `${POLICY_LABELS[row.policy] ?? row.policy} / ${WORKER_LABELS[row.workers] ?? row.workers}`; + if ( + row.worker_hint && + row.selected_policy && + row.selected_workers + ) { + return `${POLICY_LABELS[row.policy] ?? row.policy} / ${workerHintLabel(row.worker_hint)} (selected ${selectedPlanLabel(row)})`; + } + return `${POLICY_LABELS[row.policy] ?? row.policy} / ${workerPoolLabel(row.workers)}`; } function workerSortKey(workers) { @@ -1613,6 +1645,12 @@

Raw policy table

workers.appendChild(item); }); root.appendChild(workers); + + const note = document.createElement("p"); + note.className = "inline-callout"; + note.textContent = + "Adaptive rows are grouped by worker hint. Tooltips and the raw table show the fixed plan the selector actually chose for that load."; + root.appendChild(note); } function renderPolicyCommentary(data) { @@ -1652,10 +1690,19 @@

Raw policy table

}); const winnerSeq = rankedLoads - .map(({ rows }) => rows[0].workers) + .map(({ rows }) => selectedWorkers(rows[0])) .join(" → "); - const staticWinsAll = rankedLoads.every( - ({ rows }) => rows[0].policy === "static_per_worker", + const sameWinnerFamily = rankedLoads.every( + ({ rows }) => + rows[0].policy === rankedLoads[0].rows[0].policy, + ); + const adaptiveLeadsAnyLoad = rankedLoads.some( + ({ rows }) => rows[0].policy === "adaptive_shard_routing", + ); + const adaptiveFrontPack = rankedLoads.every(({ rows }) => + rows + .slice(0, 3) + .some((row) => row.policy === "adaptive_shard_routing"), ); const dedicatedAlwaysLast = rankedLoads.every( ({ rows }) => @@ -1670,9 +1717,9 @@

Raw policy table

`The front pair overlap on confidence intervals for ${tiedFrontLoads.join(", ")}, so trust those loads as close races rather than clean one-policy wins.`, ); } - if (staticWinsAll) { + if (sameWinnerFamily) { lines.push( - "Static round-robin with one delta per worker wins every tested load on this machine, so the simpler pooled-worker path is setting the pace in this study.", + `${POLICY_LABELS[rankedLoads[0].rows[0].policy] ?? rankedLoads[0].rows[0].policy} leads every tested load on this machine, so one policy family is clearly setting the pace in this study.`, ); } if (winnerSeq === "1w → 4w → 8w") { @@ -1680,6 +1727,15 @@

Raw policy table

"As load rises, the winning worker pool widens step by step. Small work prefers less coordination; heavier work finally justifies a wider pool.", ); } + if (adaptiveLeadsAnyLoad) { + lines.push( + "The workload-aware selector does win at least one tested load, which means the heuristic is not just a convenience wrapper around the slower half of the field.", + ); + } else if (adaptiveFrontPack) { + lines.push( + "The workload-aware selector stays in the front pack across the sweep, even when a fixed policy edges it out on individual loads.", + ); + } if (dedicatedAlwaysLast) { lines.push( "The dedicated one-thread-per-shard extreme never catches the field. It is useful as a boundary case, not as a promising default.", @@ -1918,7 +1974,7 @@

Raw policy table

.on("mouseenter", function (event, d) { showTooltip( event, - `${caseLabel(d)}
Load: ${d.load}
Mean: ${fmtNs(d.mean_ns)}
95% CI: ${fmtNs(d.lb_ns ?? d.mean_ns)} – ${fmtNs(d.ub_ns ?? d.mean_ns)}`, + `${caseLabel(d)}
Load: ${d.load}
Executed plan: ${selectedPlanLabel(d)}
Mean: ${fmtNs(d.mean_ns)}
95% CI: ${fmtNs(d.lb_ns ?? d.mean_ns)} – ${fmtNs(d.ub_ns ?? d.mean_ns)}`, ); }) .on("mousemove", moveTooltip) @@ -2088,7 +2144,8 @@

Raw policy table

(row) => ` ${row.load} ${POLICY_LABELS[row.policy] ?? row.policy} - ${WORKER_LABELS[row.workers] ?? row.workers} + ${row.worker_hint ? workerHintLabel(row.worker_hint) : workerPoolLabel(row.workers)} + ${selectedPlanLabel(row)} ${fmtNs(row.mean_ns)} ${fmtNs(row.lb_ns ?? row.mean_ns)} – ${fmtNs(row.ub_ns ?? row.mean_ns)} `, @@ -2099,8 +2156,9 @@

Raw policy table

Load - Policy - Workers + Strategy + Input workers + Executed plan Mean 95% CI diff --git a/docs/benchmarks/parallel-policy-matrix.json b/docs/benchmarks/parallel-policy-matrix.json index 1afe5011..41413352 100644 --- a/docs/benchmarks/parallel-policy-matrix.json +++ b/docs/benchmarks/parallel-policy-matrix.json @@ -1,8 +1,8 @@ { "group": "parallel_policy_matrix", - "baked_at": "2026-03-29T19:51:34.009982Z", - "baked_git_sha": "c235096", - "baked_source_digest": "7510f1cdabaeb08c4550190c225794d4871c9fb18a916d639ad4f42eb8367521", + "baked_at": "2026-04-01T06:24:35.490722Z", + "baked_git_sha": "5ddcf20", + "baked_source_digest": "594a91a30330109d7d00700e9966e11f2c5ca12f4dba1f78231b5db639c0917d", "template_path": "docs/benchmarks/index.html", "machine": { "os": "macos", @@ -12,394 +12,676 @@ }, "criterion_root": "target/criterion/parallel_policy_matrix", "results": [ + { + "policy": "adaptive_shard_routing", + "workers": "1w", + "worker_hint": "1w", + "selected_policy": "static_per_worker", + "selected_workers": "1w", + "selected_series": "static_per_worker:1w", + "load": 100, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_1w__selected_static_per_worker_1w/100/new/estimates.json", + "mean_ns": 45170.70683538968, + "lb_ns": 44038.050397627456, + "ub_ns": 46400.69903834069, + "series": "adaptive_shard_routing:1w" + }, + { + "policy": "adaptive_shard_routing", + "workers": "1w", + "worker_hint": "1w", + "selected_policy": "static_per_worker", + "selected_workers": "1w", + "selected_series": "static_per_worker:1w", + "load": 1000, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_1w__selected_static_per_worker_1w/1000/new/estimates.json", + "mean_ns": 320441.07359758386, + "lb_ns": 306843.07447798236, + "ub_ns": 336464.889460925, + "series": "adaptive_shard_routing:1w" + }, + { + "policy": "adaptive_shard_routing", + "workers": "1w", + "worker_hint": "1w", + "selected_policy": "static_per_worker", + "selected_workers": "1w", + "selected_series": "static_per_worker:1w", + "load": 10000, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_1w__selected_static_per_worker_1w/10000/new/estimates.json", + "mean_ns": 3352257.4166431827, + "lb_ns": 3172515.393349267, + "ub_ns": 3573891.5918121217, + "series": "adaptive_shard_routing:1w" + }, { "policy": "dynamic_per_shard", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_1w/100/new/estimates.json", - "mean_ns": 55464.43408260571, - "lb_ns": 54756.69361375341, - "ub_ns": 56202.66931437411, + "mean_ns": 50515.702017362266, + "lb_ns": 49960.85533819769, + "ub_ns": 51117.00262479456, "series": "dynamic_per_shard:1w" }, { "policy": "dynamic_per_shard", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_1w/1000/new/estimates.json", - "mean_ns": 343427.6850951146, - "lb_ns": 319083.4070900986, - "ub_ns": 373475.23771829135, + "mean_ns": 286184.76952351123, + "lb_ns": 283682.75430253963, + "ub_ns": 289134.9265411066, "series": "dynamic_per_shard:1w" }, { "policy": "dynamic_per_shard", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_1w/10000/new/estimates.json", - "mean_ns": 4201208.348823542, - "lb_ns": 3875169.7980738874, - "ub_ns": 4567399.876901627, + "mean_ns": 3236890.6028111214, + "lb_ns": 3163598.152532645, + "ub_ns": 3320956.2942565517, "series": "dynamic_per_shard:1w" }, { "policy": "dynamic_per_worker", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_1w/100/new/estimates.json", - "mean_ns": 118001.1679377424, - "lb_ns": 106180.36814934696, - "ub_ns": 135372.81707939805, + "mean_ns": 43548.817486473665, + "lb_ns": 42872.155940598466, + "ub_ns": 44275.748149573905, "series": "dynamic_per_worker:1w" }, { "policy": "dynamic_per_worker", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_1w/1000/new/estimates.json", - "mean_ns": 270892.1481032658, - "lb_ns": 267017.7138902802, - "ub_ns": 275278.0843480539, + "mean_ns": 318679.6487854434, + "lb_ns": 315356.96473102306, + "ub_ns": 322199.6108989417, "series": "dynamic_per_worker:1w" }, { "policy": "dynamic_per_worker", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_1w/10000/new/estimates.json", - "mean_ns": 2762096.073968301, - "lb_ns": 2691334.9446979314, - "ub_ns": 2853504.588098411, + "mean_ns": 3265831.0757960146, + "lb_ns": 3143340.217694261, + "ub_ns": 3462918.521039351, "series": "dynamic_per_worker:1w" }, { "policy": "static_per_shard", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/static_per_shard_1w/100/new/estimates.json", - "mean_ns": 68038.9133507309, - "lb_ns": 60144.881347430055, - "ub_ns": 78999.4423918486, + "mean_ns": 54106.522247873305, + "lb_ns": 53144.77043251318, + "ub_ns": 55081.11348974757, "series": "static_per_shard:1w" }, { "policy": "static_per_shard", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/static_per_shard_1w/1000/new/estimates.json", - "mean_ns": 315397.2679475274, - "lb_ns": 298548.79247113975, - "ub_ns": 338033.46786511806, + "mean_ns": 299845.3279419348, + "lb_ns": 295715.3106605483, + "ub_ns": 303565.8404826931, "series": "static_per_shard:1w" }, { "policy": "static_per_shard", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/static_per_shard_1w/10000/new/estimates.json", - "mean_ns": 3253597.341064291, - "lb_ns": 3174288.7754920344, - "ub_ns": 3339159.0839692825, + "mean_ns": 3186130.8141137627, + "lb_ns": 3163129.7593584433, + "ub_ns": 3212477.160973803, "series": "static_per_shard:1w" }, { "policy": "static_per_worker", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/static_per_worker_1w/100/new/estimates.json", - "mean_ns": 51739.36454908365, - "lb_ns": 48378.98747472938, - "ub_ns": 56425.19122588916, + "mean_ns": 49368.8274620388, + "lb_ns": 48679.995617259534, + "ub_ns": 50024.507009047695, "series": "static_per_worker:1w" }, { "policy": "static_per_worker", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/static_per_worker_1w/1000/new/estimates.json", - "mean_ns": 305768.3191330779, - "lb_ns": 289677.7784319992, - "ub_ns": 325951.6131473716, + "mean_ns": 257305.82397818903, + "lb_ns": 255039.72418530256, + "ub_ns": 259595.68997921923, "series": "static_per_worker:1w" }, { "policy": "static_per_worker", "workers": "1w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/static_per_worker_1w/10000/new/estimates.json", - "mean_ns": 4339330.832882105, - "lb_ns": 3776273.4789978, - "ub_ns": 4967749.246199884, + "mean_ns": 3154200.482608165, + "lb_ns": 3029736.1942840945, + "ub_ns": 3292539.256043021, "series": "static_per_worker:1w" }, + { + "policy": "adaptive_shard_routing", + "workers": "4w", + "worker_hint": "4w", + "selected_policy": "static_per_worker", + "selected_workers": "1w", + "selected_series": "static_per_worker:1w", + "load": 100, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_4w__selected_static_per_worker_1w/100/new/estimates.json", + "mean_ns": 43992.323830592424, + "lb_ns": 43220.83627672932, + "ub_ns": 44833.9068249005, + "series": "adaptive_shard_routing:4w" + }, + { + "policy": "adaptive_shard_routing", + "workers": "4w", + "worker_hint": "4w", + "selected_policy": "dynamic_per_worker", + "selected_workers": "1w", + "selected_series": "dynamic_per_worker:1w", + "load": 1000, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_4w__selected_dynamic_per_worker_1w/1000/new/estimates.json", + "mean_ns": 301971.95382570714, + "lb_ns": 295530.88317150075, + "ub_ns": 309984.30004747375, + "series": "adaptive_shard_routing:4w" + }, + { + "policy": "adaptive_shard_routing", + "workers": "4w", + "worker_hint": "4w", + "selected_policy": "dynamic_per_shard", + "selected_workers": "4w", + "selected_series": "dynamic_per_shard:4w", + "load": 10000, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_4w__selected_dynamic_per_shard_4w/10000/new/estimates.json", + "mean_ns": 2673776.0130713526, + "lb_ns": 2604270.7341417857, + "ub_ns": 2751947.8874801314, + "series": "adaptive_shard_routing:4w" + }, { "policy": "dynamic_per_shard", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_4w/100/new/estimates.json", - "mean_ns": 94623.96109289848, - "lb_ns": 92672.9358013662, - "ub_ns": 96862.53591063726, + "mean_ns": 82055.1527511099, + "lb_ns": 79511.75543375462, + "ub_ns": 84721.74470579976, "series": "dynamic_per_shard:4w" }, { "policy": "dynamic_per_shard", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_4w/1000/new/estimates.json", - "mean_ns": 291516.0789781246, - "lb_ns": 285026.3822294185, - "ub_ns": 299125.50646760175, + "mean_ns": 288027.92681351874, + "lb_ns": 279967.69307321985, + "ub_ns": 302963.3954730564, "series": "dynamic_per_shard:4w" }, { "policy": "dynamic_per_shard", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_4w/10000/new/estimates.json", - "mean_ns": 2312237.6050062696, - "lb_ns": 2254378.0920585655, - "ub_ns": 2388842.6003074846, + "mean_ns": 2422040.149857522, + "lb_ns": 2389424.481864121, + "ub_ns": 2457889.268942081, "series": "dynamic_per_shard:4w" }, { "policy": "dynamic_per_worker", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_4w/100/new/estimates.json", - "mean_ns": 480978.59250441974, - "lb_ns": 426663.4850814179, - "ub_ns": 547182.7573886289, + "mean_ns": 80464.9074086326, + "lb_ns": 78042.19605937475, + "ub_ns": 82783.11099808023, "series": "dynamic_per_worker:4w" }, { "policy": "dynamic_per_worker", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_4w/1000/new/estimates.json", - "mean_ns": 277423.6160682042, - "lb_ns": 274378.7112326404, - "ub_ns": 280653.77858380805, + "mean_ns": 349301.1054956669, + "lb_ns": 344478.52609163814, + "ub_ns": 354165.24001226487, "series": "dynamic_per_worker:4w" }, { "policy": "dynamic_per_worker", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_4w/10000/new/estimates.json", - "mean_ns": 3532714.3493538103, - "lb_ns": 3196782.458081174, - "ub_ns": 3893028.476573624, + "mean_ns": 2798819.5315694204, + "lb_ns": 2740667.369277322, + "ub_ns": 2858140.9693026, "series": "dynamic_per_worker:4w" }, { "policy": "static_per_shard", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/static_per_shard_4w/100/new/estimates.json", - "mean_ns": 99863.61987207436, - "lb_ns": 98266.36378482977, - "ub_ns": 101794.624786117, + "mean_ns": 81496.7381324212, + "lb_ns": 79490.72739457357, + "ub_ns": 83668.56433620017, "series": "static_per_shard:4w" }, { "policy": "static_per_shard", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/static_per_shard_4w/1000/new/estimates.json", - "mean_ns": 296294.6607022254, - "lb_ns": 288343.79662623204, - "ub_ns": 307105.6393947541, + "mean_ns": 273243.89711718255, + "lb_ns": 271390.4225985688, + "ub_ns": 275576.25361808186, "series": "static_per_shard:4w" }, { "policy": "static_per_shard", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/static_per_shard_4w/10000/new/estimates.json", - "mean_ns": 3076581.0265688575, - "lb_ns": 2788832.930038881, - "ub_ns": 3400977.1287433044, + "mean_ns": 2538494.8250080533, + "lb_ns": 2493325.2607828225, + "ub_ns": 2590587.585276198, "series": "static_per_shard:4w" }, { "policy": "static_per_worker", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/static_per_worker_4w/100/new/estimates.json", - "mean_ns": 82828.97430229129, - "lb_ns": 79154.03257528166, - "ub_ns": 87389.61768361439, + "mean_ns": 92265.76157562473, + "lb_ns": 90995.64145554138, + "ub_ns": 93494.11468083777, "series": "static_per_worker:4w" }, { "policy": "static_per_worker", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/static_per_worker_4w/1000/new/estimates.json", - "mean_ns": 380211.0759995546, - "lb_ns": 327855.16356549604, - "ub_ns": 462963.92953876726, + "mean_ns": 276999.48782287294, + "lb_ns": 275834.50363322074, + "ub_ns": 278111.14116521436, "series": "static_per_worker:4w" }, { "policy": "static_per_worker", "workers": "4w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/static_per_worker_4w/10000/new/estimates.json", - "mean_ns": 4262819.759565299, - "lb_ns": 3869786.074998774, - "ub_ns": 4673047.668490899, + "mean_ns": 3784507.143609345, + "lb_ns": 3646396.9359374866, + "ub_ns": 3988502.02934999, "series": "static_per_worker:4w" }, + { + "policy": "adaptive_shard_routing", + "workers": "8w", + "worker_hint": "8w", + "selected_policy": "static_per_worker", + "selected_workers": "1w", + "selected_series": "static_per_worker:1w", + "load": 100, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_8w__selected_static_per_worker_1w/100/new/estimates.json", + "mean_ns": 41649.445077542005, + "lb_ns": 40927.88017958355, + "ub_ns": 42482.481800850146, + "series": "adaptive_shard_routing:8w" + }, + { + "policy": "adaptive_shard_routing", + "workers": "8w", + "worker_hint": "8w", + "selected_policy": "dynamic_per_worker", + "selected_workers": "1w", + "selected_series": "dynamic_per_worker:1w", + "load": 1000, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_8w__selected_dynamic_per_worker_1w/1000/new/estimates.json", + "mean_ns": 326092.33179679874, + "lb_ns": 321261.1925643069, + "ub_ns": 330953.5572374113, + "series": "adaptive_shard_routing:8w" + }, + { + "policy": "adaptive_shard_routing", + "workers": "8w", + "worker_hint": "8w", + "selected_policy": "dynamic_per_shard", + "selected_workers": "4w", + "selected_series": "dynamic_per_shard:4w", + "load": 10000, + "path": "target/criterion/parallel_policy_matrix/adaptive_shard_routing__hint_8w__selected_dynamic_per_shard_4w/10000/new/estimates.json", + "mean_ns": 2522432.115796646, + "lb_ns": 2462773.1367397923, + "ub_ns": 2595560.216251987, + "series": "adaptive_shard_routing:8w" + }, { "policy": "dynamic_per_shard", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_8w/100/new/estimates.json", - "mean_ns": 152460.101521985, - "lb_ns": 142689.2895253969, - "ub_ns": 169578.32318578326, + "mean_ns": 140205.51979412173, + "lb_ns": 135864.07048469654, + "ub_ns": 144647.94829752753, "series": "dynamic_per_shard:8w" }, { "policy": "dynamic_per_shard", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_8w/1000/new/estimates.json", - "mean_ns": 427597.4980816598, - "lb_ns": 380942.18165343726, - "ub_ns": 493478.6946695924, + "mean_ns": 330258.647134717, + "lb_ns": 324277.1188797893, + "ub_ns": 339607.02609701356, "series": "dynamic_per_shard:8w" }, { "policy": "dynamic_per_shard", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_shard_8w/10000/new/estimates.json", - "mean_ns": 2876309.416375992, - "lb_ns": 2594111.144678711, - "ub_ns": 3223226.433645327, + "mean_ns": 2557485.098412513, + "lb_ns": 2487846.6331850705, + "ub_ns": 2643893.313182536, "series": "dynamic_per_shard:8w" }, { "policy": "dynamic_per_worker", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_8w/100/new/estimates.json", - "mean_ns": 157455.9425359909, - "lb_ns": 153942.67719904362, - "ub_ns": 161920.5141127526, + "mean_ns": 149511.95815615234, + "lb_ns": 147470.62017485485, + "ub_ns": 151573.26186183703, "series": "dynamic_per_worker:8w" }, { "policy": "dynamic_per_worker", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_8w/1000/new/estimates.json", - "mean_ns": 339257.17203846463, - "lb_ns": 315584.75279452157, - "ub_ns": 380663.61674345355, + "mean_ns": 325590.0612194114, + "lb_ns": 324091.42368059576, + "ub_ns": 327137.0547241333, "series": "dynamic_per_worker:8w" }, { "policy": "dynamic_per_worker", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/dynamic_per_worker_8w/10000/new/estimates.json", - "mean_ns": 2621356.7823611028, - "lb_ns": 2493971.8226580755, - "ub_ns": 2774790.902435505, + "mean_ns": 3085805.2240987127, + "lb_ns": 2920772.721915216, + "ub_ns": 3348289.549019437, "series": "dynamic_per_worker:8w" }, { "policy": "static_per_shard", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/static_per_shard_8w/100/new/estimates.json", - "mean_ns": 236985.7440476028, - "lb_ns": 221566.6802469599, - "ub_ns": 256156.75997772816, + "mean_ns": 147356.25805304147, + "lb_ns": 145053.02833667208, + "ub_ns": 149686.32319773582, "series": "static_per_shard:8w" }, { "policy": "static_per_shard", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/static_per_shard_8w/1000/new/estimates.json", - "mean_ns": 477808.37468245544, - "lb_ns": 403653.5950145256, - "ub_ns": 583335.7173446632, + "mean_ns": 322090.02217452985, + "lb_ns": 311331.5126824062, + "ub_ns": 339788.4516180621, "series": "static_per_shard:8w" }, { "policy": "static_per_shard", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/static_per_shard_8w/10000/new/estimates.json", - "mean_ns": 2710449.0420398023, - "lb_ns": 2546788.7868638574, - "ub_ns": 2953026.784321096, + "mean_ns": 2699334.6041044453, + "lb_ns": 2610318.7511610296, + "ub_ns": 2801684.93808115, "series": "static_per_shard:8w" }, { "policy": "static_per_worker", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/static_per_worker_8w/100/new/estimates.json", - "mean_ns": 172220.2781279374, - "lb_ns": 158988.24858006404, - "ub_ns": 188894.3418611982, + "mean_ns": 145470.83287082094, + "lb_ns": 142825.38357221676, + "ub_ns": 147321.79713396358, "series": "static_per_worker:8w" }, { "policy": "static_per_worker", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/static_per_worker_8w/1000/new/estimates.json", - "mean_ns": 332053.05171778303, - "lb_ns": 323383.93442205345, - "ub_ns": 341832.6875972533, + "mean_ns": 312971.2070730934, + "lb_ns": 311539.11500526086, + "ub_ns": 314529.94585226075, "series": "static_per_worker:8w" }, { "policy": "static_per_worker", "workers": "8w", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/static_per_worker_8w/10000/new/estimates.json", - "mean_ns": 5234556.768329204, - "lb_ns": 4974722.401272965, - "ub_ns": 5536259.659252154, + "mean_ns": 3809381.5602099313, + "lb_ns": 3753360.7200898663, + "ub_ns": 3870542.960591511, "series": "static_per_worker:8w" }, { "policy": "dedicated_per_shard", "workers": "dedicated", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 100, "path": "target/criterion/parallel_policy_matrix/dedicated_per_shard/100/new/estimates.json", - "mean_ns": 1911916.2158310344, - "lb_ns": 1770192.7824185104, - "ub_ns": 2076102.2267788248, + "mean_ns": 1288925.125304984, + "lb_ns": 1268798.4553926778, + "ub_ns": 1311162.5981921842, "series": "dedicated_per_shard:dedicated" }, { "policy": "dedicated_per_shard", "workers": "dedicated", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 1000, "path": "target/criterion/parallel_policy_matrix/dedicated_per_shard/1000/new/estimates.json", - "mean_ns": 5056303.960415298, - "lb_ns": 4869380.740892483, - "ub_ns": 5269361.096465565, + "mean_ns": 4097745.441445052, + "lb_ns": 4081943.937550013, + "ub_ns": 4114397.028953228, "series": "dedicated_per_shard:dedicated" }, { "policy": "dedicated_per_shard", "workers": "dedicated", + "worker_hint": null, + "selected_policy": null, + "selected_workers": null, + "selected_series": null, "load": 10000, "path": "target/criterion/parallel_policy_matrix/dedicated_per_shard/10000/new/estimates.json", - "mean_ns": 7810389.708333333, - "lb_ns": 7603201.088333334, - "ub_ns": 8024619.369652779, + "mean_ns": 7683962.817640886, + "lb_ns": 7195839.476383331, + "ub_ns": 8359661.276558725, "series": "dedicated_per_shard:dedicated" } ] diff --git a/docs/benchmarks/report-inline.html b/docs/benchmarks/report-inline.html index 166d1c2f..d78d711a 100644 --- a/docs/benchmarks/report-inline.html +++ b/docs/benchmarks/report-inline.html @@ -7,2012 +7,11 @@ Echo Benchmarks +:where(html){--font-system-ui:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;--font-transitional:Charter,Bitstream Charter,Sitka Text,Cambria,serif;--font-old-style:Iowan Old Style,Palatino Linotype,URW Palladio L,P052,serif;--font-humanist:Seravek,Gill Sans Nova,Ubuntu,Calibri,DejaVu Sans,source-sans-pro,sans-serif;--font-geometric-humanist:Avenir,Montserrat,Corbel,URW Gothic,source-sans-pro,sans-serif;--font-classical-humanist:Optima,Candara,Noto Sans,source-sans-pro,sans-serif;--font-neo-grotesque:Inter,Roboto,Helvetica Neue,Arial Nova,Nimbus Sans,Arial,sans-serif;--font-monospace-slab-serif:Nimbus Mono PS,Courier New,monospace;--font-monospace-code:Dank Mono,Operator Mono,Inconsolata,Fira Mono,ui-monospace,SF Mono,Monaco,Droid Sans Mono,Source Code Pro,Cascadia Code,Menlo,Consolas,DejaVu Sans Mono,monospace;--font-industrial:Bahnschrift,DIN Alternate,Franklin Gothic Medium,Nimbus Sans Narrow,sans-serif-condensed,sans-serif;--font-rounded-sans:ui-rounded,Hiragino Maru Gothic ProN,Quicksand,Comfortaa,Manjari,Arial Rounded MT,Arial Rounded MT Bold,Calibri,source-sans-pro,sans-serif;--font-slab-serif:Rockwell,Rockwell Nova,Roboto Slab,DejaVu Serif,Sitka Small,serif;--font-antique:Superclarendon,Bookman Old Style,URW Bookman,URW Bookman L,Georgia Pro,Georgia,serif;--font-didone:Didot,Bodoni MT,Noto Serif Display,URW Palladio L,P052,Sylfaen,serif;--font-handwritten:Segoe Print,Bradley Hand,Chilanka,TSCu_Comic,casual,cursive;--font-sans:var(--font-system-ui);--font-serif:ui-serif,serif;--font-mono:var(--font-monospace-code);--font-weight-1:100;--font-weight-2:200;--font-weight-3:300;--font-weight-4:400;--font-weight-5:500;--font-weight-6:600;--font-weight-7:700;--font-weight-8:800;--font-weight-9:900;--font-lineheight-00:.95;--font-lineheight-0:1.1;--font-lineheight-1:1.25;--font-lineheight-2:1.375;--font-lineheight-3:1.5;--font-lineheight-4:1.75;--font-lineheight-5:2;--font-letterspacing-0:-.05em;--font-letterspacing-1:.025em;--font-letterspacing-2:.050em;--font-letterspacing-3:.075em;--font-letterspacing-4:.150em;--font-letterspacing-5:.500em;--font-letterspacing-6:.750em;--font-letterspacing-7:1em;--font-size-00:.5rem;--font-size-0:.75rem;--font-size-1:1rem;--font-size-2:1.1rem;--font-size-3:1.25rem;--font-size-4:1.5rem;--font-size-5:2rem;--font-size-6:2.5rem;--font-size-7:3rem;--font-size-8:3.5rem;--font-size-fluid-0:max(.75rem,min(2vw,1rem));--font-size-fluid-1:max(1rem,min(4vw,1.5rem));--font-size-fluid-2:max(1.5rem,min(6vw,2.5rem));--font-size-fluid-3:max(2rem,min(9vw,3.5rem));--size-000:-.5rem;--size-00:-.25rem;--size-1:.25rem;--size-2:.5rem;--size-3:1rem;--size-4:1.25rem;--size-5:1.5rem;--size-6:1.75rem;--size-7:2rem;--size-8:3rem;--size-9:4rem;--size-10:5rem;--size-11:7.5rem;--size-12:10rem;--size-13:15rem;--size-14:20rem;--size-15:30rem;--size-px-000:-8px;--size-px-00:-4px;--size-px-1:4px;--size-px-2:8px;--size-px-3:16px;--size-px-4:20px;--size-px-5:24px;--size-px-6:28px;--size-px-7:32px;--size-px-8:48px;--size-px-9:64px;--size-px-10:80px;--size-px-11:120px;--size-px-12:160px;--size-px-13:240px;--size-px-14:320px;--size-px-15:480px;--size-fluid-1:max(.5rem,min(1vw,1rem));--size-fluid-2:max(1rem,min(2vw,1.5rem));--size-fluid-3:max(1.5rem,min(3vw,2rem));--size-fluid-4:max(2rem,min(4vw,3rem));--size-fluid-5:max(4rem,min(5vw,5rem));--size-fluid-6:max(5rem,min(7vw,7.5rem));--size-fluid-7:max(7.5rem,min(10vw,10rem));--size-fluid-8:max(10rem,min(20vw,15rem));--size-fluid-9:max(15rem,min(30vw,20rem));--size-fluid-10:max(20rem,min(40vw,30rem));--size-content-1:20ch;--size-content-2:45ch;--size-content-3:60ch;--size-header-1:20ch;--size-header-2:25ch;--size-header-3:35ch;--size-xxs:240px;--size-xs:360px;--size-sm:480px;--size-md:768px;--size-lg:1024px;--size-xl:1440px;--size-xxl:1920px;--size-relative-000:-.5ch;--size-relative-00:-.25ch;--size-relative-1:.25ch;--size-relative-2:.5ch;--size-relative-3:1ch;--size-relative-4:1.25ch;--size-relative-5:1.5ch;--size-relative-6:1.75ch;--size-relative-7:2ch;--size-relative-8:3ch;--size-relative-9:4ch;--size-relative-10:5ch;--size-relative-11:7.5ch;--size-relative-12:10ch;--size-relative-13:15ch;--size-relative-14:20ch;--size-relative-15:30ch;--ease-1:cubic-bezier(.25,0,.5,1);--ease-2:cubic-bezier(.25,0,.4,1);--ease-3:cubic-bezier(.25,0,.3,1);--ease-4:cubic-bezier(.25,0,.2,1);--ease-5:cubic-bezier(.25,0,.1,1);--ease-in-1:cubic-bezier(.25,0,1,1);--ease-in-2:cubic-bezier(.50,0,1,1);--ease-in-3:cubic-bezier(.70,0,1,1);--ease-in-4:cubic-bezier(.90,0,1,1);--ease-in-5:cubic-bezier(1,0,1,1);--ease-out-1:cubic-bezier(0,0,.75,1);--ease-out-2:cubic-bezier(0,0,.50,1);--ease-out-3:cubic-bezier(0,0,.3,1);--ease-out-4:cubic-bezier(0,0,.1,1);--ease-out-5:cubic-bezier(0,0,0,1);--ease-in-out-1:cubic-bezier(.1,0,.9,1);--ease-in-out-2:cubic-bezier(.3,0,.7,1);--ease-in-out-3:cubic-bezier(.5,0,.5,1);--ease-in-out-4:cubic-bezier(.7,0,.3,1);--ease-in-out-5:cubic-bezier(.9,0,.1,1);--ease-elastic-out-1:cubic-bezier(.5,.75,.75,1.25);--ease-elastic-out-2:cubic-bezier(.5,1,.75,1.25);--ease-elastic-out-3:cubic-bezier(.5,1.25,.75,1.25);--ease-elastic-out-4:cubic-bezier(.5,1.5,.75,1.25);--ease-elastic-out-5:cubic-bezier(.5,1.75,.75,1.25);--ease-elastic-in-1:cubic-bezier(.5,-0.25,.75,1);--ease-elastic-in-2:cubic-bezier(.5,-0.50,.75,1);--ease-elastic-in-3:cubic-bezier(.5,-0.75,.75,1);--ease-elastic-in-4:cubic-bezier(.5,-1.00,.75,1);--ease-elastic-in-5:cubic-bezier(.5,-1.25,.75,1);--ease-elastic-in-out-1:cubic-bezier(.5,-.1,.1,1.5);--ease-elastic-in-out-2:cubic-bezier(.5,-.3,.1,1.5);--ease-elastic-in-out-3:cubic-bezier(.5,-.5,.1,1.5);--ease-elastic-in-out-4:cubic-bezier(.5,-.7,.1,1.5);--ease-elastic-in-out-5:cubic-bezier(.5,-.9,.1,1.5);--ease-step-1:steps(2);--ease-step-2:steps(3);--ease-step-3:steps(4);--ease-step-4:steps(7);--ease-step-5:steps(10);--ease-elastic-1:var(--ease-elastic-out-1);--ease-elastic-2:var(--ease-elastic-out-2);--ease-elastic-3:var(--ease-elastic-out-3);--ease-elastic-4:var(--ease-elastic-out-4);--ease-elastic-5:var(--ease-elastic-out-5);--ease-squish-1:var(--ease-elastic-in-out-1);--ease-squish-2:var(--ease-elastic-in-out-2);--ease-squish-3:var(--ease-elastic-in-out-3);--ease-squish-4:var(--ease-elastic-in-out-4);--ease-squish-5:var(--ease-elastic-in-out-5);--ease-spring-1:linear(0,0.006,0.025 2.8%,0.101 6.1%,0.539 18.9%,0.721 25.3%,0.849 31.5%,0.937 38.1%,0.968 41.8%,0.991 45.7%,1.006 50.1%,1.015 55%,1.017 63.9%,1.001);--ease-spring-2:linear(0,0.007,0.029 2.2%,0.118 4.7%,0.625 14.4%,0.826 19%,0.902,0.962,1.008 26.1%,1.041 28.7%,1.064 32.1%,1.07 36%,1.061 40.5%,1.015 53.4%,0.999 61.6%,0.995 71.2%,1);--ease-spring-3:linear(0,0.009,0.035 2.1%,0.141 4.4%,0.723 12.9%,0.938 16.7%,1.017,1.077,1.121,1.149 24.3%,1.159,1.163,1.161,1.154 29.9%,1.129 32.8%,1.051 39.6%,1.017 43.1%,0.991,0.977 51%,0.974 53.8%,0.975 57.1%,0.997 69.8%,1.003 76.9%,1);--ease-spring-4:linear(0,0.009,0.037 1.7%,0.153 3.6%,0.776 10.3%,1.001,1.142 16%,1.185,1.209 19%,1.215 19.9% 20.8%,1.199,1.165 25%,1.056 30.3%,1.008 33%,0.973,0.955 39.2%,0.953 41.1%,0.957 43.3%,0.998 53.3%,1.009 59.1% 63.7%,0.998 78.9%,1);--ease-spring-5:linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1);--ease-bounce-1:linear(0,0.004,0.016,0.035,0.063,0.098,0.141,0.191,0.25,0.316,0.391 36.8%,0.563,0.766,1 58.8%,0.946,0.908 69.1%,0.895,0.885,0.879,0.878,0.879,0.885,0.895,0.908 89.7%,0.946,1);--ease-bounce-2:linear(0,0.004,0.016,0.035,0.063,0.098,0.141 15.1%,0.25,0.391,0.562,0.765,1,0.892 45.2%,0.849,0.815,0.788,0.769,0.757,0.753,0.757,0.769,0.788,0.815,0.85,0.892 75.2%,1 80.2%,0.973,0.954,0.943,0.939,0.943,0.954,0.973,1);--ease-bounce-3:linear(0,0.004,0.016,0.035,0.062,0.098,0.141 11.4%,0.25,0.39,0.562,0.764,1 30.3%,0.847 34.8%,0.787,0.737,0.699,0.672,0.655,0.65,0.656,0.672,0.699,0.738,0.787,0.847 61.7%,1 66.2%,0.946,0.908,0.885 74.2%,0.879,0.878,0.879,0.885 79.5%,0.908,0.946,1 87.4%,0.981,0.968,0.96,0.957,0.96,0.968,0.981,1);--ease-bounce-4:linear(0,0.004,0.016 3%,0.062,0.141,0.25,0.391,0.562 18.2%,1 24.3%,0.81,0.676 32.3%,0.629,0.595,0.575,0.568,0.575,0.595,0.629,0.676 48.2%,0.811,1 56.2%,0.918,0.86,0.825,0.814,0.825,0.86,0.918,1 77.2%,0.94 80.6%,0.925,0.92,0.925,0.94 87.5%,1 90.9%,0.974,0.965,0.974,1);--ease-bounce-5:linear(0,0.004,0.016 2.5%,0.063,0.141,0.25 10.1%,0.562,1 20.2%,0.783,0.627,0.534 30.9%,0.511,0.503,0.511,0.534 38%,0.627,0.782,1 48.7%,0.892,0.815,0.769 56.3%,0.757,0.753,0.757,0.769 61.3%,0.815,0.892,1 68.8%,0.908 72.4%,0.885,0.878,0.885,0.908 79.4%,1 83%,0.954 85.5%,0.943,0.939,0.943,0.954 90.5%,1 93%,0.977,0.97,0.977,1);--ease-circ-in:cubic-bezier(.6,.04,.98,.335);--ease-circ-in-out:cubic-bezier(.785,.135,.15,.86);--ease-circ-out:cubic-bezier(.075,.82,.165,1);--ease-cubic-in:cubic-bezier(.55,.055,.675,.19);--ease-cubic-in-out:cubic-bezier(.645,.045,.355,1);--ease-cubic-out:cubic-bezier(.215,.61,.355,1);--ease-expo-in:cubic-bezier(.95,.05,.795,.035);--ease-expo-in-out:cubic-bezier(1,0,0,1);--ease-expo-out:cubic-bezier(.19,1,.22,1);--ease-quad-in:cubic-bezier(.55,.085,.68,.53);--ease-quad-in-out:cubic-bezier(.455,.03,.515,.955);--ease-quad-out:cubic-bezier(.25,.46,.45,.94);--ease-quart-in:cubic-bezier(.895,.03,.685,.22);--ease-quart-in-out:cubic-bezier(.77,0,.175,1);--ease-quart-out:cubic-bezier(.165,.84,.44,1);--ease-quint-in:cubic-bezier(.755,.05,.855,.06);--ease-quint-in-out:cubic-bezier(.86,0,.07,1);--ease-quint-out:cubic-bezier(.23,1,.32,1);--ease-sine-in:cubic-bezier(.47,0,.745,.715);--ease-sine-in-out:cubic-bezier(.445,.05,.55,.95);--ease-sine-out:cubic-bezier(.39,.575,.565,1);--layer-1:1;--layer-2:2;--layer-3:3;--layer-4:4;--layer-5:5;--layer-important:2147483647;--shadow-color:220 3% 15%;--shadow-strength:1%;--inner-shadow-highlight:inset 0 -.5px 0 0 #fff,inset 0 .5px 0 0 rgba(0,0,0,.067);--shadow-1:0 1px 2px -1px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 9%));--shadow-2:0 3px 5px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 3%)),0 7px 14px -5px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 5%));--shadow-3:0 -1px 3px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 2%)),0 1px 2px -5px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 2%)),0 2px 5px -5px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 4%)),0 4px 12px -5px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 5%)),0 12px 15px -5px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 7%));--shadow-4:0 -2px 5px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 2%)),0 1px 1px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 3%)),0 2px 2px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 3%)),0 5px 5px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 4%)),0 9px 9px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 5%)),0 16px 16px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 6%));--shadow-5:0 -1px 2px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 2%)),0 2px 1px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 3%)),0 5px 5px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 3%)),0 10px 10px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 4%)),0 20px 20px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 5%)),0 40px 40px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 7%));--shadow-6:0 -1px 2px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 2%)),0 3px 2px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 3%)),0 7px 5px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 3%)),0 12px 10px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 4%)),0 22px 18px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 5%)),0 41px 33px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 6%)),0 100px 80px -2px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 7%));--inner-shadow-0:inset 0 0 0 1px hsl(var(--shadow-color)/calc(var(--shadow-strength) + 9%));--inner-shadow-1:inset 0 1px 2px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 9%)),var(--inner-shadow-highlight);--inner-shadow-2:inset 0 1px 4px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 9%)),var(--inner-shadow-highlight);--inner-shadow-3:inset 0 2px 8px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 9%)),var(--inner-shadow-highlight);--inner-shadow-4:inset 0 2px 14px 0 hsl(var(--shadow-color)/calc(var(--shadow-strength) + 9%)),var(--inner-shadow-highlight);--ratio-square:1;--ratio-landscape:4/3;--ratio-portrait:3/4;--ratio-widescreen:16/9;--ratio-ultrawide:18/5;--ratio-golden:1.6180/1;--gray-0:#f8f9fa;--gray-1:#f1f3f5;--gray-2:#e9ecef;--gray-3:#dee2e6;--gray-4:#ced4da;--gray-5:#adb5bd;--gray-6:#868e96;--gray-7:#495057;--gray-8:#343a40;--gray-9:#212529;--gray-10:#16191d;--gray-11:#0d0f12;--gray-12:#030507;--stone-0:#f8fafb;--stone-1:#f2f4f6;--stone-2:#ebedef;--stone-3:#e0e4e5;--stone-4:#d1d6d8;--stone-5:#b1b6b9;--stone-6:#979b9d;--stone-7:#7e8282;--stone-8:#666968;--stone-9:#50514f;--stone-10:#3a3a37;--stone-11:#252521;--stone-12:#121210;--red-0:#fff5f5;--red-1:#ffe3e3;--red-2:#ffc9c9;--red-3:#ffa8a8;--red-4:#ff8787;--red-5:#ff6b6b;--red-6:#fa5252;--red-7:#f03e3e;--red-8:#e03131;--red-9:#c92a2a;--red-10:#b02525;--red-11:#962020;--red-12:#7d1a1a;--pink-0:#fff0f6;--pink-1:#ffdeeb;--pink-2:#fcc2d7;--pink-3:#faa2c1;--pink-4:#f783ac;--pink-5:#f06595;--pink-6:#e64980;--pink-7:#d6336c;--pink-8:#c2255c;--pink-9:#a61e4d;--pink-10:#8c1941;--pink-11:#731536;--pink-12:#59102a;--purple-0:#f8f0fc;--purple-1:#f3d9fa;--purple-2:#eebefa;--purple-3:#e599f7;--purple-4:#da77f2;--purple-5:#cc5de8;--purple-6:#be4bdb;--purple-7:#ae3ec9;--purple-8:#9c36b5;--purple-9:#862e9c;--purple-10:#702682;--purple-11:#5a1e69;--purple-12:#44174f;--violet-0:#f3f0ff;--violet-1:#e5dbff;--violet-2:#d0bfff;--violet-3:#b197fc;--violet-4:#9775fa;--violet-5:#845ef7;--violet-6:#7950f2;--violet-7:#7048e8;--violet-8:#6741d9;--violet-9:#5f3dc4;--violet-10:#5235ab;--violet-11:#462d91;--violet-12:#3a2578;--indigo-0:#edf2ff;--indigo-1:#dbe4ff;--indigo-2:#bac8ff;--indigo-3:#91a7ff;--indigo-4:#748ffc;--indigo-5:#5c7cfa;--indigo-6:#4c6ef5;--indigo-7:#4263eb;--indigo-8:#3b5bdb;--indigo-9:#364fc7;--indigo-10:#2f44ad;--indigo-11:#283a94;--indigo-12:#21307a;--blue-0:#e7f5ff;--blue-1:#d0ebff;--blue-2:#a5d8ff;--blue-3:#74c0fc;--blue-4:#4dabf7;--blue-5:#339af0;--blue-6:#228be6;--blue-7:#1c7ed6;--blue-8:#1971c2;--blue-9:#1864ab;--blue-10:#145591;--blue-11:#114678;--blue-12:#0d375e;--cyan-0:#e3fafc;--cyan-1:#c5f6fa;--cyan-2:#99e9f2;--cyan-3:#66d9e8;--cyan-4:#3bc9db;--cyan-5:#22b8cf;--cyan-6:#15aabf;--cyan-7:#1098ad;--cyan-8:#0c8599;--cyan-9:#0b7285;--cyan-10:#095c6b;--cyan-11:#074652;--cyan-12:#053038;--teal-0:#e6fcf5;--teal-1:#c3fae8;--teal-2:#96f2d7;--teal-3:#63e6be;--teal-4:#38d9a9;--teal-5:#20c997;--teal-6:#12b886;--teal-7:#0ca678;--teal-8:#099268;--teal-9:#087f5b;--teal-10:#066649;--teal-11:#054d37;--teal-12:#033325;--green-0:#ebfbee;--green-1:#d3f9d8;--green-2:#b2f2bb;--green-3:#8ce99a;--green-4:#69db7c;--green-5:#51cf66;--green-6:#40c057;--green-7:#37b24d;--green-8:#2f9e44;--green-9:#2b8a3e;--green-10:#237032;--green-11:#1b5727;--green-12:#133d1b;--lime-0:#f4fce3;--lime-1:#e9fac8;--lime-2:#d8f5a2;--lime-3:#c0eb75;--lime-4:#a9e34b;--lime-5:#94d82d;--lime-6:#82c91e;--lime-7:#74b816;--lime-8:#66a80f;--lime-9:#5c940d;--lime-10:#4c7a0b;--lime-11:#3c6109;--lime-12:#2c4706;--yellow-0:#fff9db;--yellow-1:#fff3bf;--yellow-2:#ffec99;--yellow-3:#ffe066;--yellow-4:#ffd43b;--yellow-5:#fcc419;--yellow-6:#fab005;--yellow-7:#f59f00;--yellow-8:#f08c00;--yellow-9:#e67700;--yellow-10:#b35c00;--yellow-11:#804200;--yellow-12:#663500;--orange-0:#fff4e6;--orange-1:#ffe8cc;--orange-2:#ffd8a8;--orange-3:#ffc078;--orange-4:#ffa94d;--orange-5:#ff922b;--orange-6:#fd7e14;--orange-7:#f76707;--orange-8:#e8590c;--orange-9:#d9480f;--orange-10:#bf400d;--orange-11:#99330b;--orange-12:#802b09;--choco-0:#fff8dc;--choco-1:#fce1bc;--choco-2:#f7ca9e;--choco-3:#f1b280;--choco-4:#e99b62;--choco-5:#df8545;--choco-6:#d46e25;--choco-7:#bd5f1b;--choco-8:#a45117;--choco-9:#8a4513;--choco-10:#703a13;--choco-11:#572f12;--choco-12:#3d210d;--brown-0:#faf4eb;--brown-1:#ede0d1;--brown-2:#e0cab7;--brown-3:#d3b79e;--brown-4:#c5a285;--brown-5:#b78f6d;--brown-6:#a87c56;--brown-7:#956b47;--brown-8:#825b3a;--brown-9:#6f4b2d;--brown-10:#5e3a21;--brown-11:#4e2b15;--brown-12:#422412;--sand-0:#f8fafb;--sand-1:#e6e4dc;--sand-2:#d5cfbd;--sand-3:#c2b9a0;--sand-4:#aea58c;--sand-5:#9a9178;--sand-6:#867c65;--sand-7:#736a53;--sand-8:#5f5746;--sand-9:#4b4639;--sand-10:#38352d;--sand-11:#252521;--sand-12:#121210;--camo-0:#f9fbe7;--camo-1:#e8ed9c;--camo-2:#d2df4e;--camo-3:#c2ce34;--camo-4:#b5bb2e;--camo-5:#a7a827;--camo-6:#999621;--camo-7:#8c851c;--camo-8:#7e7416;--camo-9:#6d6414;--camo-10:#5d5411;--camo-11:#4d460e;--camo-12:#36300a;--jungle-0:#ecfeb0;--jungle-1:#def39a;--jungle-2:#d0e884;--jungle-3:#c2dd6e;--jungle-4:#b5d15b;--jungle-5:#a8c648;--jungle-6:#9bbb36;--jungle-7:#8fb024;--jungle-8:#84a513;--jungle-9:#7a9908;--jungle-10:#658006;--jungle-11:#516605;--jungle-12:#3d4d04;--gradient-space: ;--gradient-1:linear-gradient(to bottom right var(--gradient-space),#1f005c,#5b0060,#870160,#ac255e,#ca485c,#e16b5c,#f39060,#ffb56b);--gradient-2:linear-gradient(to bottom right var(--gradient-space),#48005c,#8300e2,#a269ff);--gradient-3:radial-gradient(circle at top right var(--gradient-space),#0ff,rgba(0,255,255,0)),radial-gradient(circle at bottom left var(--gradient-space),#ff1492,rgba(255,20,146,0));--gradient-4:linear-gradient(to bottom right var(--gradient-space),#00f5a0,#00d9f5);--gradient-5:conic-gradient(from -270deg at 75% 110% var(--gradient-space),#f0f,#fffaf0);--gradient-6:conic-gradient(from -90deg at top left var(--gradient-space),#000,#fff);--gradient-7:linear-gradient(to bottom right var(--gradient-space),#72c6ef,#004e8f);--gradient-8:conic-gradient(from 90deg at 50% 0% var(--gradient-space),#111,50%,#222,#111);--gradient-9:conic-gradient(from .5turn at bottom center var(--gradient-space),#add8e6,#fff);--gradient-10:conic-gradient(from 90deg at 40% -25% var(--gradient-space),gold,#f79d03,#ee6907,#e6390a,#de0d0d,#d61039,#cf1261,#c71585,#cf1261,#d61039,#de0d0d,#ee6907,#f79d03,gold,gold,gold);--gradient-11:conic-gradient(at bottom left var(--gradient-space),#ff1493,cyan);--gradient-12:conic-gradient(from 90deg at 25% -10% var(--gradient-space),#ff4500,#d3f340,#7bee85,#afeeee,#7bee85);--gradient-13:radial-gradient(circle at 50% 200% var(--gradient-space),#000142,#3b0083,#b300c3,#ff059f,#ff4661,#ffad86,#fff3c7);--gradient-14:conic-gradient(at top right var(--gradient-space),lime,cyan);--gradient-15:linear-gradient(to bottom right var(--gradient-space),#c7d2fe,#fecaca,#fef3c7);--gradient-16:radial-gradient(circle at 50% -250% var(--gradient-space),#374151,#111827,#000);--gradient-17:conic-gradient(from -90deg at 50% -25% var(--gradient-space),blue,#8a2be2);--gradient-18:linear-gradient(0deg var(--gradient-space),rgba(255,0,0,.8),rgba(255,0,0,0) 75%),linear-gradient(60deg var(--gradient-space),rgba(255,255,0,.8),rgba(255,255,0,0) 75%),linear-gradient(120deg var(--gradient-space),rgba(0,255,0,.8),rgba(0,255,0,0) 75%),linear-gradient(180deg var(--gradient-space),rgba(0,255,255,.8),rgba(0,255,255,0) 75%),linear-gradient(240deg var(--gradient-space),rgba(0,0,255,.8),rgba(0,0,255,0) 75%),linear-gradient(300deg var(--gradient-space),rgba(255,0,255,.8),rgba(255,0,255,0) 75%);--gradient-19:linear-gradient(to bottom right var(--gradient-space),#ffe259,#ffa751);--gradient-20:conic-gradient(from -135deg at -10% center var(--gradient-space),orange,#ff7715,#ff522a,#ff3f47,#ff5482,#ff69b4);--gradient-21:conic-gradient(from -90deg at 25% 115% var(--gradient-space),red,#f06,#f0c,#c0f,#60f,#00f,#00f,#00f,#00f);--gradient-22:linear-gradient(to bottom right var(--gradient-space),#acb6e5,#86fde8);--gradient-23:linear-gradient(to bottom right var(--gradient-space),#536976,#292e49);--gradient-24:conic-gradient(from .5turn at 0% 0% var(--gradient-space),#00c476,10%,#82b0ff,90%,#00c476);--gradient-25:conic-gradient(at 125% 50% var(--gradient-space),#b78cf7,#ff7c94,#ffcf0d,#ff7c94,#b78cf7);--gradient-26:linear-gradient(to bottom right var(--gradient-space),#9796f0,#fbc7d4);--gradient-27:conic-gradient(from .5turn at bottom left var(--gradient-space),#ff1493,#639);--gradient-28:conic-gradient(from -90deg at 50% 105% var(--gradient-space),#fff,orchid);--gradient-29:radial-gradient(circle at top right var(--gradient-space),#bfb3ff,rgba(191,179,255,0)),radial-gradient(circle at bottom left var(--gradient-space),#86acf9,rgba(134,172,249,0));--gradient-30:radial-gradient(circle at top right var(--gradient-space),#00ff80,rgba(0,255,128,0)),radial-gradient(circle at bottom left var(--gradient-space),#adffd6,rgba(173,255,214,0));--noise-1:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.005' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");--noise-2:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 300 300' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.05' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");--noise-3:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.25' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");--noise-4:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 2056 2056' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.5' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");--noise-5:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 2056 2056' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.75' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)'/%3E%3C/svg%3E");--noise-filter-1:contrast(300%) brightness(100%);--noise-filter-2:contrast(200%) brightness(150%);--noise-filter-3:contrast(200%) brightness(250%);--noise-filter-4:contrast(200%) brightness(500%);--noise-filter-5:contrast(200%) brightness(1000%);--animation-fade-in:fade-in .5s var(--ease-3);--animation-fade-in-bloom:fade-in-bloom 2s var(--ease-3);--animation-fade-out:fade-out .5s var(--ease-3);--animation-fade-out-bloom:fade-out-bloom 2s var(--ease-3);--animation-scale-up:scale-up .5s var(--ease-3);--animation-scale-down:scale-down .5s var(--ease-3);--animation-slide-out-up:slide-out-up .5s var(--ease-3);--animation-slide-out-down:slide-out-down .5s var(--ease-3);--animation-slide-out-right:slide-out-right .5s var(--ease-3);--animation-slide-out-left:slide-out-left .5s var(--ease-3);--animation-slide-in-up:slide-in-up .5s var(--ease-3);--animation-slide-in-down:slide-in-down .5s var(--ease-3);--animation-slide-in-right:slide-in-right .5s var(--ease-3);--animation-slide-in-left:slide-in-left .5s var(--ease-3);--animation-shake-x:shake-x .75s var(--ease-out-5);--animation-shake-y:shake-y .75s var(--ease-out-5);--animation-shake-z:shake-z 1s var(--ease-in-out-3);--animation-spin:spin 2s linear infinite;--animation-ping:ping 5s var(--ease-out-3) infinite;--animation-blink:blink 1s var(--ease-out-3) infinite;--animation-float:float 3s var(--ease-in-out-3) infinite;--animation-bounce:bounce 2s var(--ease-squish-2) infinite;--animation-pulse:pulse 2s var(--ease-out-3) infinite;--border-size-1:1px;--border-size-2:2px;--border-size-3:5px;--border-size-4:10px;--border-size-5:25px;--radius-1:2px;--radius-2:5px;--radius-3:1rem;--radius-4:2rem;--radius-5:4rem;--radius-6:8rem;--radius-drawn-1:255px 15px 225px 15px/15px 225px 15px 255px;--radius-drawn-2:125px 10px 20px 185px/25px 205px 205px 25px;--radius-drawn-3:15px 255px 15px 225px/225px 15px 255px 15px;--radius-drawn-4:15px 25px 155px 25px/225px 150px 25px 115px;--radius-drawn-5:250px 25px 15px 20px/15px 80px 105px 115px;--radius-drawn-6:28px 100px 20px 15px/150px 30px 205px 225px;--radius-round:1e5px;--radius-blob-1:30% 70% 70% 30%/53% 30% 70% 47%;--radius-blob-2:53% 47% 34% 66%/63% 46% 54% 37%;--radius-blob-3:37% 63% 56% 44%/49% 56% 44% 51%;--radius-blob-4:63% 37% 37% 63%/43% 37% 63% 57%;--radius-blob-5:49% 51% 48% 52%/57% 44% 56% 43%;--radius-conditional-1:clamp(0px,calc(100vw - 100%) * 1e5,var(--radius-1));--radius-conditional-2:clamp(0px,calc(100vw - 100%) * 1e5,var(--radius-2));--radius-conditional-3:clamp(0px,calc(100vw - 100%) * 1e5,var(--radius-3));--radius-conditional-4:clamp(0px,calc(100vw - 100%) * 1e5,var(--radius-4));--radius-conditional-5:clamp(0px,calc(100vw - 100%) * 1e5,var(--radius-5));--radius-conditional-6:clamp(0px,calc(100vw - 100%) * 1e5,var(--radius-6))}@media (prefers-color-scheme:dark){:where(html){--shadow-color:220 40% 2%;--shadow-strength:25%;--inner-shadow-highlight:inset 0 -.5px 0 0 hsla(0,0%,100%,.067),inset 0 .5px 0 0 rgba(0,0,0,.467)}}@supports (background:linear-gradient(to right in oklab,#000,#fff)){:where(html){--gradient-space:in oklab}}@keyframes fade-in{to{opacity:1}}@keyframes fade-in-bloom{0%{filter:brightness(1) blur(20px);opacity:0}10%{filter:brightness(2) blur(10px);opacity:1}to{filter:brightness(1) blur(0);opacity:1}}@keyframes fade-out{to{opacity:0}}@keyframes fade-out-bloom{to{filter:brightness(1) blur(20px);opacity:0}10%{filter:brightness(2) blur(10px);opacity:1}0%{filter:brightness(1) blur(0);opacity:1}}@keyframes scale-up{to{transform:scale(1.25)}}@keyframes scale-down{to{transform:scale(.75)}}@keyframes slide-out-up{to{transform:translateY(-100%)}}@keyframes slide-out-down{to{transform:translateY(100%)}}@keyframes slide-out-right{to{transform:translateX(100%)}}@keyframes slide-out-left{to{transform:translateX(-100%)}}@keyframes slide-in-up{0%{transform:translateY(100%)}}@keyframes slide-in-down{0%{transform:translateY(-100%)}}@keyframes slide-in-right{0%{transform:translateX(-100%)}}@keyframes slide-in-left{0%{transform:translateX(100%)}}@keyframes shake-x{0%,to{transform:translateX(0)}20%{transform:translateX(-5%)}40%{transform:translateX(5%)}60%{transform:translateX(-5%)}80%{transform:translateX(5%)}}@keyframes shake-y{0%,to{transform:translateY(0)}20%{transform:translateY(-5%)}40%{transform:translateY(5%)}60%{transform:translateY(-5%)}80%{transform:translateY(5%)}}@keyframes shake-z{0%,to{transform:rotate(0deg)}20%{transform:rotate(-2deg)}40%{transform:rotate(2deg)}60%{transform:rotate(-2deg)}80%{transform:rotate(2deg)}}@keyframes spin{to{transform:rotate(1turn)}}@keyframes ping{90%,to{opacity:0;transform:scale(2)}}@keyframes blink{0%,to{opacity:1}50%{opacity:.5}}@keyframes float{50%{transform:translateY(-25%)}}@keyframes bounce{25%{transform:translateY(-20%)}40%{transform:translateY(-3%)}0%,60%,to{transform:translateY(0)}}@keyframes pulse{50%{transform:scale(.9)}}@media (prefers-color-scheme:dark){@keyframes fade-in-bloom{0%{filter:brightness(1) blur(20px);opacity:0}10%{filter:brightness(.5) blur(10px);opacity:1}to{filter:brightness(1) blur(0);opacity:1}}}@media (prefers-color-scheme:dark){@keyframes fade-out-bloom{to{filter:brightness(1) blur(20px);opacity:0}10%{filter:brightness(.5) blur(10px);opacity:1}0%{filter:brightness(1) blur(0);opacity:1}}} + +:where(html){--csstools-color-scheme--light: ;--link:var(--indigo-3);--link-visited:var(--purple-3);--text-1:var(--gray-0);--text-2:var(--gray-4);--surface-1:var(--gray-9);--surface-2:var(--gray-8);--surface-3:var(--gray-7);--surface-4:var(--gray-6);--scrollthumb-color:var(--gray-6);-webkit-text-size-adjust:none;--shadow-strength:10%;--shadow-color:220 40% 2%;--inner-shadow-highlight:inset 0 -.5px 0 0 hsla(0,0%,100%,.067),inset 0 .5px 0 0 rgba(0,0,0,.467);accent-color:var(--brand,var(--link));background-color:var(--surface-1);block-size:100%;caret-color:var(--brand,var(--link));color:var(--text-1);color-scheme:dark;font-family:var(--font-system-ui);line-height:var(--font-lineheight-3);scrollbar-color:var(--scrollthumb-color) transparent}:where(html) :where(dialog){background-color:var(--surface-2)}:where(html) :where(button,.btn){--_highlight:var(--_highlight-dark);--_bg:var(--_bg-dark);--_ink-shadow:var(--_ink-shadow-dark)}:where(html) :where(button,.btn):where([type=reset]){--_text:var(--red-2);--_border:var(--surface-3)}:where(html) [disabled]:where(button,input[type=button],.btn){--_text:var(--gray-5)}:where(html) [disabled]:where(button,input[type=submit],.btn){--_text:var(--gray-5)}:where(html) [disabled]:where(button,input[type=reset],.btn){--_text:var(--gray-5)}:where(html) :where(textarea,select,input:not([type=button],[type=submit],[type=reset])){background-color:#171a1c}:where(html) :where([disabled]),:where(html) :where([type=reset]),:where(html) :where([type=submit]),:where(html) :where(form button:not([type=button])){--_bg:var(--surface-1)}:where(a[href]){color:var(--brand,var(--link))}:where(a[href]):where(:visited){color:var(--link-visited)}:focus-visible{outline-color:var(--brand,var(--link))}*,:after,:before{box-sizing:border-box}:where(:not(dialog)){margin:0}:where(:not(fieldset,progress,meter)){background-origin:border-box;background-repeat:no-repeat;border-style:solid;border-width:0}@media (prefers-reduced-motion:no-preference){:where(html){scroll-behavior:smooth}}@media (prefers-reduced-motion:no-preference){:where(:focus-visible){transition:outline-offset 145ms var(--ease-2)}:where(:not(:active):focus-visible){transition-duration:.25s}}:where(:not(:active):focus-visible){outline-offset:5px}:where(body){min-block-size:100%}:where(h1,h2,h3,h4,h5,h6){text-wrap:balance;font-weight:var(--font-weight-9);line-height:var(--font-lineheight-1)}:where(h1){font-size:var(--font-size-8);max-inline-size:var(--size-header-1)}:where(h2){font-size:var(--font-size-6);max-inline-size:var(--size-header-2)}:where(h3){font-size:var(--font-size-5)}:where(h4){font-size:var(--font-size-4)}:where(h5){font-size:var(--font-size-3)}:where(h3,h4,h5,h6,dt){max-inline-size:var(--size-header-3)}:where(p,ul,ol,dl,h6){font-size:var(--font-size-2)}:where(a,u,ins,abbr){text-underline-offset:1px}@supports (-moz-appearance:none){:where(a,u,ins,abbr){text-underline-offset:2px}}:where(a[href],area,button,input:not([type=text],[type=email],[type=number],[type=password],[type=""],[type=tel],[type=url]),label[for],select,summary,[tabindex]:not([tabindex*="-"],pre)){cursor:pointer}:where(a[href],area,button,input,label[for],select,summary,textarea,[tabindex]:not([tabindex*="-"])){-webkit-tap-highlight-color:transparent;touch-action:manipulation}:where(a):where([href]){text-decoration-color:var(--indigo-2)}:where(a):where([href]):where(:visited){text-decoration-color:var(--purple-2)}:where(a):where(:not(:hover)){text-decoration:inherit}:where(img,svg,video,canvas,audio,iframe,embed,object){display:block}:where(img,svg,video){block-size:auto;max-inline-size:100%}:where(input,button,textarea,select),:where(input[type=file])::-webkit-file-upload-button{color:inherit;font:inherit;font-size:inherit;letter-spacing:inherit}::placeholder{color:var(--gray-7);color:var(--gray-6);opacity:.75}:where(input:not([type=range]),textarea){padding-block:var(--size-1);padding-inline:var(--size-2)}:where(select){field-sizing:content;padding-block:.75ch;padding-inline:var(--size-relative-4) 0}:where(textarea,select,input:not([type=button],[type=submit],[type=reset])){background-color:var(--surface-2);background-color:var(--gray-10);border-radius:var(--radius-2)}:where(textarea){field-sizing:content;min-block-size:2lh;min-inline-size:var(--size-content-1);resize:vertical}:where(input[type=checkbox],input[type=radio]){block-size:var(--size-3);inline-size:var(--size-3)}:where(svg:not([width])){inline-size:var(--size-10)}:where(code,kbd,samp,pre){font-family:var(--font-monospace-code),monospace}:where(:not(pre)>code,kbd){white-space:nowrap}:where(pre){direction:ltr;max-inline-size:max-content;min-inline-size:0;white-space:pre;writing-mode:lr}:where(:not(pre)>code){background:var(--surface-2);border-radius:var(--radius-2);padding:var(--size-1) var(--size-2);writing-mode:lr}:where(kbd,var){border-color:var(--surface-4);border-radius:var(--radius-2);border-width:var(--border-size-1);padding:var(--size-1) var(--size-2)}:where(mark){border-radius:var(--radius-2);padding-inline:var(--size-1)}:where(ol,ul){padding-inline-start:var(--size-8)}:where(li){padding-inline-start:var(--size-2)}:where(li,dd,figcaption){max-inline-size:var(--size-content-2)}:where(p){text-wrap:pretty;max-inline-size:var(--size-content-3)}:where(dt,summary){font-weight:var(--font-weight-7)}:where(dt:not(:first-of-type)){margin-block-start:var(--size-5)}:where(small){font-size:max(.5em,var(--font-size-0));max-inline-size:var(--size-content-1)}:where(hr){background-color:var(--surface-3);height:var(--border-size-2);margin-block:var(--size-fluid-5)}:where(figure){display:grid;gap:var(--size-2);place-items:center}:where(figure)>:where(figcaption){text-wrap:balance;font-size:var(--font-size-1)}:where(blockquote,:not(blockquote)>cite){border-inline-start-width:var(--border-size-3)}:where(blockquote){display:grid;gap:var(--size-3);max-inline-size:var(--size-content-2);padding-block:var(--size-3);padding-inline:var(--size-4)}:where(:not(blockquote)>cite){padding-inline-start:var(--size-2)}:where(summary){background:var(--surface-3);border-radius:var(--radius-2);margin:calc(var(--size-2)*-1) calc(var(--size-3)*-1);padding:var(--size-2) var(--size-3)}:where(details){background:var(--surface-2);border-radius:var(--radius-2);padding-block:var(--size-2);padding-inline:var(--size-3)}:where(details[open]>summary){border-end-end-radius:0;border-end-start-radius:0;margin-bottom:var(--size-2)}:where(fieldset){border:var(--border-size-1) solid var(--surface-4);border-radius:var(--radius-2)}:where(del){background:var(--red-9);color:var(--red-2)}:where(ins){background:var(--green-9);color:var(--green-1)}:where(abbr){text-decoration-color:var(--blue-5)}:where(dialog){background-color:var(--surface-1);background-color:var(--surface-2);border-radius:var(--radius-3);box-shadow:var(--shadow-6);color:inherit}:where(menu){display:flex;gap:var(--size-3);padding-inline-start:0}:where(sup){font-size:.5em}:where(table){--nice-inner-radius:calc(var(--radius-3) - 2px);background:var(--surface-2);border:1px solid var(--surface-2);border-radius:var(--radius-3);width:fit-content}:where(table:not(:has(tfoot)) tr:last-child td:first-child){border-end-start-radius:var(--nice-inner-radius)}:where(table:not(:has(tfoot)) tr:last-child td:last-child){border-end-end-radius:var(--nice-inner-radius)}:where(table thead tr:first-child th:first-child){border-start-start-radius:var(--nice-inner-radius)}:where(table thead tr:first-child th:last-child){border-start-end-radius:var(--nice-inner-radius)}:where(tfoot tr:last-child th:first-of-type){border-end-start-radius:var(--nice-inner-radius)}:where(tfoot tr:last-child td:first-of-type){border-end-start-radius:var(--nice-inner-radius)}:where(tfoot tr:last-child th:last-of-type){border-end-end-radius:var(--nice-inner-radius)}:where(tfoot tr:last-child td:last-of-type){border-end-end-radius:var(--nice-inner-radius)}:where(th){background-color:var(--surface-2);color:var(--text-1)}:where(table a:not(.does-not-exist):focus-visible){outline-offset:-2px}:where(table button:not(.does-not-exist):focus-visible){outline-offset:-2px}:where(table [contenteditable]:focus-visible){outline-offset:-2px}:where(td){text-wrap:pretty;background:var(--surface-1);max-inline-size:var(--size-content-2)}:where(td,th){padding:var(--size-2);text-align:left}:where(td:not([align])){text-align:center}:where(th:not([align])){text-align:center}:where(thead){border-collapse:collapse}:where(table tr:hover td),:where(tbody tr:nth-child(2n):hover td){background-color:var(--surface-3)}:where(table>caption){margin:var(--size-3)}:where(tfoot button){padding-block:var(--size-1);padding-inline:var(--size-3)} +