tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 56cc5dc4589285464b4fb2076228082ee87a13ec
parent 9035a5f813ace390e4ac29f96da367a4839b06ea
Author: triesap <tyson@radroots.org>
Date:   Wed, 17 Jun 2026 16:51:43 -0700

bench: add v1 mvp release gates

Adds the virtual-relay tenancy benchmark profile, release acceptance coverage, and source invariants for the Tangle v1 MVP multi-tenant boundary.

Diffstat:
Mcrates/tangle/tests/release_acceptance.rs | 7++++++-
Mcrates/tangle/tests/source_invariant.rs | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_bench/src/bin/tangle_benchmark_report.rs | 16+++++++++++-----
Mcrates/tangle_bench/src/lib.rs | 454+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mflake.nix | 7++++++-
5 files changed, 549 insertions(+), 13 deletions(-)

diff --git a/crates/tangle/tests/release_acceptance.rs b/crates/tangle/tests/release_acceptance.rs @@ -23,10 +23,15 @@ fn release_acceptance_app_covers_release_candidate_validation_ladder() { "cargo clippy --workspace --all-targets -- -D warnings", "cargo test --workspace", "cargo test -p tangle_runtime --test base_relay_v2", + "cargo test -p tangle_runtime isolation", + "cargo test -p tangle_runtime server", + "cargo test -p tangle_runtime auth", + "cargo test -p tangle_runtime backup", + "cargo test -p tangle_runtime export", "cargo test -p tangle_groups", "cargo test -p tangle_store_pocket", "cargo test -p tangle_bench", - "cargo run -p tangle_bench --bin tangle-benchmark-report", + "cargo run -p tangle_bench --bin tangle-benchmark-report -- --profile virtual-relay-tenancy", ] { assert!( release.contains(required), diff --git a/crates/tangle/tests/source_invariant.rs b/crates/tangle/tests/source_invariant.rs @@ -67,6 +67,84 @@ fn tenant_runtime_surface_has_no_stale_single_runtime_api_names() { } #[test] +fn tangle_v1_mvp_source_invariants_guard_tenancy_boundaries() { + let workspace_root = workspace_root(); + let config_source = + fs::read_to_string(workspace_root.join("crates/tangle_runtime/src/config.rs")) + .expect("config source"); + let server_source = + fs::read_to_string(workspace_root.join("crates/tangle_runtime/src/server.rs")) + .expect("server source"); + let host_source = fs::read_to_string(workspace_root.join("crates/tangle_runtime/src/host.rs")) + .expect("host source"); + + assert!( + config_source.contains("fn reject_legacy_single_relay_config"), + "host and tenant config parsing must keep an explicit old-config rejection gate" + ); + assert!( + config_source.contains("legacy single-relay config is not supported"), + "old single-relay config compatibility must remain rejected" + ); + assert!( + config_source.contains("at least one active tenant is required"), + "host config must not synthesize a default tenant when no active tenant exists" + ); + assert!( + config_source.contains("insert_unique(\"pocket data directory\""), + "tenant config validation must reject shared Pocket store directories" + ); + assert!( + host_source.contains("tenants_by_host: BTreeMap<CanonicalHost, TenantRuntimeEntry>"), + "host runtime must keep host-keyed virtual relay serving state" + ); + assert!( + host_source.contains("host_by_tenant_id: BTreeMap<TenantId, CanonicalHost>"), + "host runtime must keep tenant-id lookup separate from host routing" + ); + assert!( + server_source + .contains(".tenant_by_host(&host)\n .ok_or(HostResolutionError::Unknown)"), + "relay request routing must fail closed when the host is not a configured tenant" + ); + let tenant_resolution = server_source + .find("let tenant = match resolve_tenant") + .expect("tenant resolution"); + let websocket_path = server_source + .find("match websocket") + .expect("websocket path"); + assert!( + tenant_resolution < websocket_path, + "server must resolve the tenant before entering websocket or NIP-11 request handling" + ); + + let mut source_files = Vec::new(); + collect_rust_files( + &workspace_root.join("crates/tangle_runtime/src"), + &mut source_files, + ); + collect_rust_files(&workspace_root.join("crates/tangle/src"), &mut source_files); + for path in source_files { + let source = fs::read_to_string(&path).expect("source file"); + for forbidden in [ + "default_tenant", + "fallback_tenant", + "default tenant", + "fallback tenant", + "no multi-tenancy", + ] { + assert!( + !source.contains(forbidden), + "{} contains forbidden tenancy compatibility text `{forbidden}`", + path.strip_prefix(&workspace_root) + .unwrap_or(path.as_path()) + .display() + ); + } + } +} + +#[test] fn scanner_removes_test_gated_items_without_removing_production_items() { let source = [ "#[cfg(test)]\n", diff --git a/crates/tangle_bench/src/bin/tangle_benchmark_report.rs b/crates/tangle_bench/src/bin/tangle_benchmark_report.rs @@ -80,7 +80,7 @@ impl BenchmarkReportArgs { fn parse(args: impl IntoIterator<Item = String>) -> Result<Option<Self>, String> { let mut output_root = PathBuf::from(".local/tangle/benchmarks"); let mut run_id = None; - let mut profile_name = BenchmarkProfileName::Smoke; + let mut profile_name = BenchmarkProfileName::VirtualRelayTenancy; let mut config = BenchDatasetConfig::smoke(); let mut dataset_overridden = false; let mut thresholds_json = None; @@ -266,7 +266,7 @@ fn path_string(path: &Path) -> String { fn help_text() -> String { [ - "usage: tangle-benchmark-report [--output-root PATH] [--run-id ID] [--profile smoke|medium|large-smoke|proof-10m|proof-large-group|proof-join-storm|proof-slow-client]", + "usage: tangle-benchmark-report [--output-root PATH] [--run-id ID] [--profile smoke|virtual-relay-tenancy|medium|large-smoke|proof-10m|proof-large-group|proof-join-storm|proof-slow-client]", " [--thresholds-json PATH] [--target-hardware-evidence TEXT]", " [--group-count COUNT] [--public-events-per-group COUNT]", " [--private-events-per-group COUNT] [--public-note-count COUNT]", @@ -281,14 +281,20 @@ mod tests { use tangle_bench::{BenchDatasetConfig, BenchmarkProfileName}; #[test] - fn benchmark_report_args_default_to_smoke_profile() { + fn benchmark_report_args_default_to_virtual_relay_tenancy_profile() { let args = BenchmarkReportArgs::parse(["--run-id".to_owned(), "unit".to_owned()]) .expect("parse") .expect("args"); - assert_eq!(args.profile.name(), BenchmarkProfileName::Smoke); + assert_eq!( + args.profile.name(), + BenchmarkProfileName::VirtualRelayTenancy + ); assert_eq!(args.profile.dataset_config(), BenchDatasetConfig::smoke()); - assert_eq!(args.profile.threshold_source(), "builtin:smoke"); + assert_eq!( + args.profile.threshold_source(), + "builtin:virtual-relay-tenancy" + ); } #[test] diff --git a/crates/tangle_bench/src/lib.rs b/crates/tangle_bench/src/lib.rs @@ -42,6 +42,9 @@ pub const SCENARIO_PROJECTION_REBUILD: &str = "projection_rebuild"; pub const SCENARIO_OUTBOX_REPLAY: &str = "outbox_replay"; pub const SCENARIO_BROADCAST_LAG: &str = "broadcast_lag"; pub const SCENARIO_MEMORY_PROFILE: &str = "memory_profile"; +pub const SCENARIO_VIRTUAL_RELAY_FANOUT_1_PERCENT: &str = "virtual_relay_fanout_1_percent"; +pub const SCENARIO_VIRTUAL_RELAY_FANOUT_10_PERCENT: &str = "virtual_relay_fanout_10_percent"; +pub const SCENARIO_VIRTUAL_RELAY_FANOUT_100_PERCENT: &str = "virtual_relay_fanout_100_percent"; pub const POCKET_SOURCE_REPOSITORY: &str = "https://github.com/triesap/pocket"; pub const POCKET_SOURCE_REVISION: &str = "329334f20948c796c6016b673b92551ac4855ad7"; @@ -130,6 +133,7 @@ impl BenchDatasetConfig { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BenchmarkProfileName { Smoke, + VirtualRelayTenancy, Medium, LargeSmoke, Proof10m, @@ -142,6 +146,7 @@ impl BenchmarkProfileName { pub fn parse(value: &str) -> Result<Self, String> { match value { "smoke" => Ok(Self::Smoke), + "virtual-relay-tenancy" => Ok(Self::VirtualRelayTenancy), "medium" => Ok(Self::Medium), "large-smoke" => Ok(Self::LargeSmoke), "proof-10m" => Ok(Self::Proof10m), @@ -149,7 +154,7 @@ impl BenchmarkProfileName { "proof-join-storm" => Ok(Self::ProofJoinStorm), "proof-slow-client" => Ok(Self::ProofSlowClient), _ => Err(format!( - "unknown benchmark profile `{value}`; expected smoke, medium, large-smoke, proof-10m, proof-large-group, proof-join-storm, or proof-slow-client" + "unknown benchmark profile `{value}`; expected smoke, virtual-relay-tenancy, medium, large-smoke, proof-10m, proof-large-group, proof-join-storm, or proof-slow-client" )), } } @@ -157,6 +162,7 @@ impl BenchmarkProfileName { pub fn as_str(self) -> &'static str { match self { Self::Smoke => "smoke", + Self::VirtualRelayTenancy => "virtual-relay-tenancy", Self::Medium => "medium", Self::LargeSmoke => "large-smoke", Self::Proof10m => "proof-10m", @@ -166,9 +172,10 @@ impl BenchmarkProfileName { } } - pub fn all() -> [Self; 7] { + pub fn all() -> [Self; 8] { [ Self::Smoke, + Self::VirtualRelayTenancy, Self::Medium, Self::LargeSmoke, Self::Proof10m, @@ -199,6 +206,7 @@ impl BenchmarkProfile { pub fn from_name(name: BenchmarkProfileName) -> Self { match name { BenchmarkProfileName::Smoke => Self::smoke(), + BenchmarkProfileName::VirtualRelayTenancy => Self::virtual_relay_tenancy(), BenchmarkProfileName::Medium => Self::medium(), BenchmarkProfileName::LargeSmoke => Self::large_smoke(), BenchmarkProfileName::Proof10m => Self::proof_10m(), @@ -216,6 +224,14 @@ impl BenchmarkProfile { ) } + pub fn virtual_relay_tenancy() -> Self { + Self::new( + BenchmarkProfileName::VirtualRelayTenancy, + BenchDatasetConfig::smoke(), + BenchmarkThresholds::large_smoke(), + ) + } + pub fn medium() -> Self { Self::new( BenchmarkProfileName::Medium, @@ -349,6 +365,73 @@ impl BenchmarkProfile { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VirtualRelayTenancyConfig { + pub tenant_count: usize, + pub subscriptions_per_tenant: usize, + pub max_pending_events_per_subscription: usize, +} + +impl VirtualRelayTenancyConfig { + pub fn mvp() -> Self { + Self { + tenant_count: 10, + subscriptions_per_tenant: 2_000, + max_pending_events_per_subscription: 16, + } + } + + fn validate(self) -> Result<Self, String> { + if self.tenant_count < 10 { + return Err("virtual relay tenancy benchmark requires at least 10 tenants".to_owned()); + } + if self.subscriptions_per_tenant < 2_000 { + return Err( + "virtual relay tenancy benchmark requires at least 2,000 subscriptions per busiest tenant" + .to_owned(), + ); + } + if self.aggregate_active_subscriptions() < 20_000 { + return Err( + "virtual relay tenancy benchmark requires at least 20,000 aggregate subscriptions" + .to_owned(), + ); + } + if self.max_pending_events_per_subscription == 0 { + return Err( + "virtual relay tenancy benchmark pending event capacity must be greater than zero" + .to_owned(), + ); + } + Ok(self) + } + + pub fn aggregate_active_subscriptions(self) -> usize { + self.tenant_count * self.subscriptions_per_tenant + } + + pub fn busiest_tenant_active_subscriptions(self) -> usize { + self.subscriptions_per_tenant + } + + fn one_percent_fanout(self) -> usize { + self.aggregate_active_subscriptions() / 100 + } + + fn ten_percent_fanout(self) -> usize { + self.aggregate_active_subscriptions() / 10 + } + + fn to_json(self) -> serde_json::Value { + json!({ + "tenant_count": self.tenant_count, + "aggregate_active_subscriptions": self.aggregate_active_subscriptions(), + "busiest_tenant_active_subscriptions": self.busiest_tenant_active_subscriptions(), + "max_pending_events_per_subscription": self.max_pending_events_per_subscription + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BenchGroupVisibility { Public, Private, @@ -647,6 +730,7 @@ pub struct ScenarioReport { pub p95_micros: u64, pub p99_micros: u64, pub max_rss_bytes: u64, + pub observations: BTreeMap<String, serde_json::Value>, } impl ScenarioReport { @@ -676,9 +760,15 @@ impl ScenarioReport { p95_micros: percentile(&samples, 95), p99_micros: percentile(&samples, 99), max_rss_bytes, + observations: BTreeMap::new(), } } + fn with_observations(mut self, observations: BTreeMap<String, serde_json::Value>) -> Self { + self.observations = observations; + self + } + fn pass_latency_gate(&self, p95_threshold_micros: u64) -> bool { self.rejected == 0 && self.accepted == self.attempted @@ -715,7 +805,8 @@ impl ScenarioReport { }, "memory": { "max_rss_bytes": self.max_rss_bytes - } + }, + "observations": &self.observations }) } } @@ -911,7 +1002,7 @@ impl BenchmarkRunReport { let outbox_replay = run_outbox_replay_benchmark(&dataset)?; let broadcast_lag = run_broadcast_lag_benchmark(&dataset)?; let memory_profile = run_memory_profile_benchmark(&dataset)?; - let scenarios = vec![ + let mut scenarios = vec![ pocket_query, read_gate, count_resource_controls, @@ -920,6 +1011,11 @@ impl BenchmarkRunReport { broadcast_lag, memory_profile, ]; + if profile.name() == BenchmarkProfileName::VirtualRelayTenancy { + scenarios.extend(run_virtual_relay_tenancy_benchmarks( + VirtualRelayTenancyConfig::mvp(), + )?); + } let validation_summary = validation_summary(&scenarios, thresholds)?; let dataset_profile = DatasetProfile::from_dataset(&dataset)?; Ok(Self { @@ -991,6 +1087,13 @@ impl BenchmarkRunReport { .target_hardware_evidence() .unwrap_or("absent") }, + "tangle_v1_mvp": { + "benchmark_evidence": virtual_relay_tenancy_summary_json( + self.profile.name(), + &self.scenarios + ), + "external_integration": external_integration_json() + }, "artifacts": { "summary_json": "summary.json", "dataset_events_jsonl": "dataset-events.jsonl" @@ -1007,6 +1110,51 @@ fn pocket_source_json() -> serde_json::Value { }) } +fn virtual_relay_tenancy_summary_json( + profile_name: BenchmarkProfileName, + scenarios: &[ScenarioReport], +) -> serde_json::Value { + let fanout_scenarios = [ + SCENARIO_VIRTUAL_RELAY_FANOUT_1_PERCENT, + SCENARIO_VIRTUAL_RELAY_FANOUT_10_PERCENT, + SCENARIO_VIRTUAL_RELAY_FANOUT_100_PERCENT, + ] + .into_iter() + .filter_map(|name| scenarios.iter().find(|scenario| scenario.scenario == name)) + .map(ScenarioReport::to_json) + .collect::<Vec<_>>(); + let status = if profile_name == BenchmarkProfileName::VirtualRelayTenancy + && fanout_scenarios.len() == 3 + { + "pass" + } else { + "not_run" + }; + json!({ + "status": status, + "required_profile": BenchmarkProfileName::VirtualRelayTenancy.as_str(), + "configured_profile": profile_name.as_str(), + "target": VirtualRelayTenancyConfig::mvp().to_json(), + "fanout_scenarios": fanout_scenarios + }) +} + +fn external_integration_json() -> serde_json::Value { + json!({ + "runner": "radroots_testing_tangle_integration", + "local_status": "unavailable", + "unavailable_command": "cargo test -p radroots_testing_tangle_integration -- tangle_v1_mvp", + "repo_boundary": "not present in the standalone Tangle workspace", + "required_scenarios": [ + "host_routing", + "tenant_isolation", + "auth_realm_isolation", + "nip29_same_group_isolation", + "backup_export_boundaries" + ] + }) +} + fn validation_overall_status(summary: &BTreeMap<String, String>) -> &'static str { if summary.values().all(|value| value == "pass") { "pass" @@ -1391,6 +1539,194 @@ fn run_memory_profile_benchmark(dataset: &BenchDataset) -> Result<ScenarioReport )) } +struct VirtualRelayTenantBench { + tenant_index: usize, + relay: BaseRelay, +} + +fn run_virtual_relay_tenancy_benchmarks( + config: VirtualRelayTenancyConfig, +) -> Result<Vec<ScenarioReport>, String> { + let config = config.validate()?; + let mut tenants = materialize_virtual_relay_tenants(config)?; + let scenarios = [ + ( + SCENARIO_VIRTUAL_RELAY_FANOUT_1_PERCENT, + "fanout-001", + 1, + config.one_percent_fanout(), + ), + ( + SCENARIO_VIRTUAL_RELAY_FANOUT_10_PERCENT, + "fanout-010", + 10, + config.ten_percent_fanout(), + ), + ( + SCENARIO_VIRTUAL_RELAY_FANOUT_100_PERCENT, + "fanout-100", + 100, + config.aggregate_active_subscriptions(), + ), + ]; + let mut reports = Vec::with_capacity(scenarios.len()); + for (scenario, tag_value, fanout_percent, expected_messages) in scenarios { + reports.push(run_virtual_relay_fanout_scenario( + &mut tenants, + config, + scenario, + tag_value, + fanout_percent, + expected_messages, + )?); + } + for tenant in &mut tenants { + tenant.relay.shutdown().map_err(|error| error.to_string())?; + } + Ok(reports) +} + +fn materialize_virtual_relay_tenants( + config: VirtualRelayTenancyConfig, +) -> Result<Vec<VirtualRelayTenantBench>, String> { + let mut tenants = Vec::with_capacity(config.tenant_count); + for tenant_index in 0..config.tenant_count { + let store_config = bench_store_config(&format!("virtual-relay-tenancy-{tenant_index}"))?; + let mut relay = BaseRelay::open( + &store_config, + relay_limits_with_subscriptions( + config.max_pending_events_per_subscription, + config.subscriptions_per_tenant, + ), + PocketQueryConfig::default(), + ) + .map_err(|error| error.to_string())?; + for subscription_index in 0..config.subscriptions_per_tenant { + let global_index = tenant_index * config.subscriptions_per_tenant + subscription_index; + let filter = virtual_relay_subscription_filter(config, tenant_index, global_index)?; + relay + .handle_pocket_req( + subscription(&format!("vr-{tenant_index:02}-{subscription_index:04}"))?, + vec![filter], + ) + .map_err(|error| error.to_string())?; + } + tenants.push(VirtualRelayTenantBench { + tenant_index, + relay, + }); + } + Ok(tenants) +} + +fn virtual_relay_subscription_filter( + config: VirtualRelayTenancyConfig, + tenant_index: usize, + global_index: usize, +) -> Result<PocketOwnedFilter, String> { + let mut tag_values = vec!["fanout-100"]; + if tenant_index == 0 { + tag_values.push("fanout-010"); + } + if global_index < config.one_percent_fanout() { + tag_values.push("fanout-001"); + } + pocket_filter_from_value(&json!({"kinds": [1], "#t": tag_values})) +} + +fn run_virtual_relay_fanout_scenario( + tenants: &mut [VirtualRelayTenantBench], + config: VirtualRelayTenancyConfig, + scenario: &str, + tag_value: &str, + fanout_percent: u64, + expected_messages: usize, +) -> Result<ScenarioReport, String> { + let started = Instant::now(); + let mut samples = Vec::new(); + let mut observed_messages = 0_usize; + let mut publish_tasks = 0_usize; + for tenant in tenants { + if !virtual_relay_tenant_participates(fanout_percent, tenant.tenant_index) { + continue; + } + let publish_started = Instant::now(); + let event = pocket_protocol_event( + FixtureKey::Member, + 1_714_800_000 + + u64::try_from(tenant.tenant_index).expect("tenant index fits in u64") + + fanout_percent, + 1, + vec![Tag::from_parts("t", &[tag_value]).map_err(|error| error.to_string())?], + &format!("virtual relay fanout {fanout_percent} percent"), + )?; + let pocket = pocket_event(&event)?; + let messages = tenant.relay.fanout_pocket(&pocket); + samples.push(elapsed_micros(publish_started)); + observed_messages += messages + .iter() + .filter(|message| matches!(message, RuntimeRelayMessage::Event { .. })) + .count(); + publish_tasks += 1; + } + let elapsed = elapsed_micros(started); + let attempted = u64::try_from(expected_messages).expect("expected message count fits in u64"); + let accepted = u64::try_from(observed_messages).expect("observed message count fits in u64"); + let rejected = attempted.saturating_sub(accepted) + accepted.saturating_sub(attempted); + let max_rss_bytes = estimate_virtual_relay_memory_bytes(config); + let mut observations = BTreeMap::new(); + observations.insert("tenant_count".to_owned(), json!(config.tenant_count)); + observations.insert( + "aggregate_active_subscriptions".to_owned(), + json!(config.aggregate_active_subscriptions()), + ); + observations.insert( + "busiest_tenant_active_subscriptions".to_owned(), + json!(config.busiest_tenant_active_subscriptions()), + ); + observations.insert("fanout_percent".to_owned(), json!(fanout_percent)); + observations.insert("publish_task_count".to_owned(), json!(publish_tasks)); + observations.insert( + "expected_enqueued_events".to_owned(), + json!(expected_messages), + ); + observations.insert( + "observed_enqueued_events".to_owned(), + json!(observed_messages), + ); + observations.insert( + "observable_queue_depth".to_owned(), + json!(observed_messages), + ); + observations.insert( + "max_pending_events_per_subscription".to_owned(), + json!(config.max_pending_events_per_subscription), + ); + observations.insert("memory_estimate_bytes".to_owned(), json!(max_rss_bytes)); + observations.insert( + "tenant_publish_model".to_owned(), + json!("one publish per targeted virtual relay tenant"), + ); + Ok(ScenarioReport::new( + scenario, + attempted, + accepted, + rejected, + elapsed, + samples, + max_rss_bytes, + ) + .with_observations(observations)) +} + +fn virtual_relay_tenant_participates(fanout_percent: u64, tenant_index: usize) -> bool { + match fanout_percent { + 1 | 10 => tenant_index == 0, + 100 => true, + _ => false, + } +} + struct CountResourceControlProbe { accepted: u64, rejected: u64, @@ -1516,10 +1852,17 @@ fn materialize_dataset( } fn relay_limits(max_pending_events: usize) -> BaseRelayLimits { + relay_limits_with_subscriptions(max_pending_events, 512) +} + +fn relay_limits_with_subscriptions( + max_pending_events: usize, + max_subscriptions: usize, +) -> BaseRelayLimits { BaseRelayLimits::new(BaseRelayLimitSettings { max_pending_events, max_subscription_id_length: 64, - max_subscriptions: 512, + max_subscriptions, max_filters_per_request: 10, max_tag_values_per_filter: 100, max_query_complexity: 610, @@ -1922,6 +2265,25 @@ fn validation_summary( ) { failures.push(failure); } + for name in [ + SCENARIO_VIRTUAL_RELAY_FANOUT_1_PERCENT, + SCENARIO_VIRTUAL_RELAY_FANOUT_10_PERCENT, + SCENARIO_VIRTUAL_RELAY_FANOUT_100_PERCENT, + ] { + let Some(report) = scenarios + .iter() + .find(|scenario| scenario.scenario.as_str() == name) + else { + continue; + }; + if let Some(failure) = record_threshold_status( + &mut summary, + name, + report.pass_latency_gate(thresholds.broadcast_lag_p95_micros), + ) { + failures.push(failure); + } + } if failures.is_empty() { Ok(summary) } else { @@ -2137,6 +2499,14 @@ fn estimate_memory_bytes(dataset: &BenchDataset) -> u64 { .expect("estimated memory fits in u64") } +fn estimate_virtual_relay_memory_bytes(config: VirtualRelayTenancyConfig) -> u64 { + let subscription_bytes = config.aggregate_active_subscriptions() * 512; + let tenant_bytes = config.tenant_count * 1024 * 1024; + (subscription_bytes + tenant_bytes) + .try_into() + .expect("estimated virtual relay memory fits in u64") +} + fn percentile(samples: &[u64], percentile: u64) -> u64 { if samples.is_empty() { return 0; @@ -2169,7 +2539,9 @@ mod tests { BenchmarkProfileName, BenchmarkRunReport, BenchmarkThresholds, POCKET_SOURCE_REPOSITORY, POCKET_SOURCE_REVISION, SCENARIO_BROADCAST_LAG, SCENARIO_COUNT_RESOURCE_CONTROLS, SCENARIO_GROUP_READ_GATE_OVERHEAD, SCENARIO_MEMORY_PROFILE, SCENARIO_OUTBOX_REPLAY, - SCENARIO_POCKET_QUERY_VISIBLE_EVENTS, SCENARIO_PROJECTION_REBUILD, ScenarioReport, + SCENARIO_POCKET_QUERY_VISIBLE_EVENTS, SCENARIO_PROJECTION_REBUILD, + SCENARIO_VIRTUAL_RELAY_FANOUT_1_PERCENT, SCENARIO_VIRTUAL_RELAY_FANOUT_10_PERCENT, + SCENARIO_VIRTUAL_RELAY_FANOUT_100_PERCENT, ScenarioReport, VirtualRelayTenancyConfig, generated_state_counts, materialize_dataset, }; use std::collections::BTreeSet; @@ -2322,6 +2694,7 @@ mod tests { .collect::<Vec<_>>(), vec![ "smoke", + "virtual-relay-tenancy", "medium", "large-smoke", "proof-10m", @@ -2337,6 +2710,12 @@ mod tests { "smoke" ); assert_eq!( + BenchmarkProfileName::parse("virtual-relay-tenancy") + .expect("virtual") + .as_str(), + "virtual-relay-tenancy" + ); + assert_eq!( BenchmarkProfileName::parse("medium") .expect("medium") .as_str(), @@ -2359,6 +2738,10 @@ mod tests { BenchDatasetConfig::smoke() ); assert_eq!( + BenchmarkProfile::virtual_relay_tenancy().dataset_config(), + BenchDatasetConfig::smoke() + ); + assert_eq!( BenchmarkProfile::medium().dataset_config(), BenchDatasetConfig::medium() ); @@ -2604,6 +2987,65 @@ mod tests { summary["artifacts"]["dataset_events_jsonl"], "dataset-events.jsonl" ); + assert_eq!( + summary["tangle_v1_mvp"]["benchmark_evidence"]["status"], + "not_run" + ); + assert_eq!( + summary["tangle_v1_mvp"]["benchmark_evidence"]["required_profile"], + "virtual-relay-tenancy" + ); + assert_eq!( + summary["tangle_v1_mvp"]["external_integration"]["runner"], + "radroots_testing_tangle_integration" + ); + assert_eq!( + summary["tangle_v1_mvp"]["external_integration"]["local_status"], + "unavailable" + ); + } + + #[test] + fn virtual_relay_tenancy_profile_contract_matches_mvp_target() { + let config = VirtualRelayTenancyConfig::mvp(); + + assert_eq!(config.tenant_count, 10); + assert_eq!(config.aggregate_active_subscriptions(), 20_000); + assert_eq!(config.busiest_tenant_active_subscriptions(), 2_000); + assert_eq!(config.one_percent_fanout(), 200); + assert_eq!(config.ten_percent_fanout(), 2_000); + assert!(config.validate().is_ok()); + assert_eq!( + BenchmarkProfile::virtual_relay_tenancy().name(), + BenchmarkProfileName::VirtualRelayTenancy + ); + } + + #[test] + fn virtual_relay_tenancy_summary_requires_three_fanout_scenarios() { + let summary = super::virtual_relay_tenancy_summary_json( + BenchmarkProfileName::VirtualRelayTenancy, + &[ + passing_scenario(SCENARIO_VIRTUAL_RELAY_FANOUT_1_PERCENT), + passing_scenario(SCENARIO_VIRTUAL_RELAY_FANOUT_10_PERCENT), + passing_scenario(SCENARIO_VIRTUAL_RELAY_FANOUT_100_PERCENT), + ], + ); + + assert_eq!(summary["status"], "pass"); + assert_eq!(summary["target"]["tenant_count"], 10); + assert_eq!(summary["target"]["aggregate_active_subscriptions"], 20_000); + assert_eq!( + summary["target"]["busiest_tenant_active_subscriptions"], + 2_000 + ); + assert_eq!( + summary["fanout_scenarios"] + .as_array() + .expect("fanout scenarios") + .len(), + 3 + ); } #[test] diff --git a/flake.nix b/flake.nix @@ -87,10 +87,15 @@ cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace cargo test -p tangle_runtime --test base_relay_v2 + cargo test -p tangle_runtime isolation + cargo test -p tangle_runtime server + cargo test -p tangle_runtime auth + cargo test -p tangle_runtime backup + cargo test -p tangle_runtime export cargo test -p tangle_groups cargo test -p tangle_store_pocket cargo test -p tangle_bench - cargo run -p tangle_bench --bin tangle-benchmark-report + cargo run -p tangle_bench --bin tangle-benchmark-report -- --profile virtual-relay-tenancy ''; in {