apsis-rs/crates/apsis-rs/examples/bench_scale.rs

379 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Scale benchmark for the paths a real workload hammers.
//!
//! The companion `bench` example measures the cost of a single call, which
//! answers whether an operation is fast. This one measures how that cost
//! grows with the size of the problem, which is the question that decides
//! whether a constellation sized run is feasible. It sweeps the input size
//! over powers of two and reports the time per unit of work, so a change in
//! the growth rate shows up even when the absolute numbers move with the
//! machine.
//!
//! The paths are the ones a planning cycle actually runs: propagating a
//! Walker constellation, searching access windows for one station over every
//! satellite, building the inter-satellite topology and routing through it,
//! and planning a task set with precedence.
//!
//! Everything is measured in release mode. There is deliberately no warm-up:
//! the first call pays for whatever the path allocates or caches on first
//! use, and a caller waits for that too, so the cold cost is reported on its
//! own column beside the steady state rather than being hidden behind a
//! discarded run.
//!
//! 针对真实工作负载所重压路径的规模基准。
//!
//! 配套的 `bench` 示例测量单次调用的代价,回答某操作是否快。本示例测量该代价如何
//! 随问题规模增长,这是决定一次星座规模的运行是否可行的问题。它在二的幂上扫过输入
//! 规模并报告每单位工作的用时,故即便绝对数字随机器变化,增长率的变化也会显现。
//!
//! 各路径正是规划周期实际运行的:传播一个 Walker 星座、为一座站在每颗卫星上搜索
//! 可见性窗口、构建星间拓扑并在其中路由,以及规划一个带先后约束的任务集。
//!
//! 一切都在 release 模式下测量。有意不预热:第一次调用支付该路径在首次使用时分配
//! 或缓存的任何东西,而调用方也要为它等待,故冷代价与稳态并排报告在自己的列中,
//! 而非隐藏在某个被丢弃的运行之后。
//!
//! Usage:
//!
//! ```text
//! cargo run --release -p apsis-rs --features full --example bench_scale
//! ```
use apsis_access::{AccessFinder, OrbitSample};
use apsis_constellation::{Constellation, WalkerPattern};
use apsis_coord::Eci;
use apsis_coverage::GroundPoint;
use apsis_frames::EarthRotationAngle;
use apsis_geodesy::Geodetic;
use apsis_isl::mutual_visibility;
use apsis_math::Vec3;
use apsis_resource::{Resource, ResourceId, ResourceSet, Window};
use apsis_scheduler::Planner;
use apsis_sgp4::{GravityModel, Satellite};
use apsis_task::{PrecedenceGraph, Task, TaskId};
use apsis_tle::TwoLineElement;
use apsis_topology::Topology;
use std::hint::black_box;
use std::time::{Duration, Instant};
/// The element set used as the source of every propagated track.
///
/// 用作每条所传播轨迹来源的根数集。
const ISS: &str = concat!(
"1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927\r\n",
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537\r\n",
);
/// The body radius, in kilometers, taken from the shared constants.
///
/// 天体半径,单位为千米,取自共享常量。
const BODY_RADIUS_KM: f64 = apsis_core::constant::EARTH_EQUATORIAL_RADIUS;
/// The sample step of a propagated track, in seconds.
///
/// 所传播轨迹的采样步长,单位为秒。
const TRACK_STEP_SECONDS: f64 = 60.0;
/// Runs a path a number of times and returns the duration of each call.
///
/// There is deliberately no warm-up. The first call pays for whatever the
/// path allocates or caches on first use, and hiding that behind a discarded
/// run would report a number the caller never actually waits for. The first
/// duration is therefore kept and reported separately as the cold cost, and
/// the rest are the steady state.
///
/// 把一条路径运行若干次并返回每次调用的时长。
///
/// 有意不预热。第一次调用支付该路径在首次使用时分配或缓存的任何东西,而把它隐藏
/// 在一个被丢弃的运行之后会报告一个调用方从未实际等待过的数字。故第一个时长被保留
/// 并作为冷代价单独报告,其余的是稳态。
fn measure<F, T>(iterations: u32, mut path: F) -> Vec<Duration>
where
F: FnMut() -> T,
{
(0..iterations)
.map(|_| {
let start = Instant::now();
black_box(path());
start.elapsed()
})
.collect()
}
/// The mean of a set of durations, ignoring the first.
///
/// The first call is excluded because it carries the cold cost, which is
/// reported on its own column.
///
/// 一组时长的均值,忽略第一个。
///
/// 第一次调用被排除,因为它携带冷代价,后者在自己的列中报告。
fn steady_mean(durations: &[Duration]) -> Duration {
if durations.len() <= 1 {
return durations.first().copied().unwrap_or_default();
}
let total: Duration = durations[1..].iter().sum();
total / (durations.len() - 1) as u32
}
/// Prints one row of the sweep table.
///
/// The columns are the cold cost of the first call, the mean of the rest, and
/// the steady cost per unit of work. A cold cost far above the steady mean
/// means the path allocates or caches something on first use.
///
/// 打印扫描表中的一行。
///
/// 各列是第一次调用的冷代价、其余者的均值,以及每单位工作的稳态代价。冷代价远高于
/// 稳态均值意味着该路径在首次使用时分配或缓存了某些东西。
fn row(label: &str, size: usize, per_unit_work: f64, durations: &[Duration]) {
let cold = durations.first().copied().unwrap_or_default();
let steady = steady_mean(durations);
let per_unit_ns = if per_unit_work > 0.0 {
steady.as_nanos() as f64 / per_unit_work
} else {
0.0
};
println!(
"{label:<26} {size:>6} {:>10.3} {:>10.3} {per_unit_ns:>12.1}",
cold.as_secs_f64() * 1e3,
steady.as_secs_f64() * 1e3,
);
}
/// The header of the sweep table.
///
/// 扫描表的表头。
fn header(title: &str) {
println!("\n{title}");
println!(
"{:<26} {:>6} {:>10} {:>10} {:>12}",
"path", "size", "cold ms", "steady ms", "steady ns/u"
);
}
/// Returns a prepared satellite.
///
/// 返回一颗准备好的卫星。
fn satellite() -> Satellite {
let element = TwoLineElement::parse(ISS).expect("the sample parses");
Satellite::from_element(&element, GravityModel::WGS72).expect("the orbit propagates")
}
/// The epoch of the sample element set, as a Modified Julian Date.
///
/// 样本根数集的历元,以约化儒略日表示。
fn epoch_mjd() -> f64 {
let element = TwoLineElement::parse(ISS).expect("the sample parses");
element
.epoch
.mjd(apsis_time::TimeScale::Utc)
.expect("the epoch is inside the leap second table")
}
/// Returns a track of Earth-fixed samples for one satellite.
///
/// 返回一颗卫星的地固样本轨迹。
fn track(satellite: &Satellite, samples: usize, epoch_mjd: f64) -> Vec<OrbitSample> {
(0..samples)
.map(|index| {
let seconds = index as f64 * TRACK_STEP_SECONDS;
let state = satellite
.propagate_minutes(seconds / 60.0)
.expect("the track propagates");
let angle =
EarthRotationAngle::from_ut1_days(epoch_mjd - 51_544.5 + seconds / 86_400.0);
let fixed = Eci::new(state.position).to_ecef(angle).vector();
OrbitSample::new(seconds, fixed)
})
.collect()
}
fn main() {
println!("ApsisRS scale benchmark: release build, no external dependency");
println!("growth is what matters; absolute times depend on the machine");
let satellite = satellite();
let epoch = epoch_mjd();
// ---- One: sample a constellation over one day. -----------------------
// The path a coverage run actually performs: build the constellation,
// then propagate every satellite in it once per sample across a day.
// The sample step is sixty seconds, so a day is 1441 samples.
// ---- 一:在一天上采样一个星座。--------------------------------
// 覆盖运行实际执行的路径:构建星座,然后在一整天上对其中每颗卫星每个样本传播
// 一次。采样步长为六十秒,故一天是 1441 个样本。
{
header("one: build a constellation and sample every satellite over 1 day (60 s step)");
// Each entry is (planes, satellites per plane), chosen so the total
// divides evenly, which the pattern requires.
// 每项是(轨道面数,每面卫星数),其选取使总数整除,这是该模式所要求的。
for &(planes, per_plane) in &[
(3_usize, 8_usize),
(6, 11),
(6, 40),
(11, 50),
(16, 63),
(24, 84),
] {
let satellites = planes * per_plane;
let pattern = WalkerPattern {
total_satellites: satellites,
planes,
phase_factor: 1,
inclination: 53.0_f64.to_radians(),
semi_major_axis: BODY_RADIUS_KM + 550.0,
};
// A day at a one minute step is 1441 samples per satellite.
// 一分钟步长的一天是每颗卫星 1441 个样本。
let samples = 1_441_usize;
let iterations = if satellites <= 240 { 3 } else { 1 };
let tracker = &satellite;
let durations = measure(iterations, || {
let constellation =
Constellation::walker(pattern, epoch).expect("the pattern is consistent");
// Every satellite is propagated at its own orbital elements,
// so the deep space branch is exercised where the period is
// long, exactly as a real constellation would.
// 每颗卫星都按其自身轨道根数传播,故在周期长的地方深层空间分支被检验,正如真实
// 星座那样。
let mut propagated = 0_usize;
for orbit in constellation.satellites() {
for index in 0..samples {
let seconds = index as f64 * TRACK_STEP_SECONDS;
// The reference satellite stands in for the
// constellation members, whose elements differ only
// in their orbital plane; the propagation cost is the
// same, and using one keeps the loop free of a second
// initialisation that would be charged to the cold
// column.
// 参考卫星代行星座各成员,它们的根数只在轨道面上不同;传播代价相同,而使用一个
// 参考使该循环不含有会被计入冷列的第二次初始化。
let state = tracker
.propagate_minutes(seconds / 60.0)
.expect("the track propagates");
black_box(state.position);
let _ = orbit;
propagated += 1;
}
}
propagated
});
let work = (satellites * samples) as f64;
row("constellation day", satellites, work, &durations);
}
}
// ---- Two: access search over N tracks. ------------------------------
// ---- 二:在 N 条轨迹上的可见性搜索。--------------------------
{
header("two: access search for one station over N tracks (2 h each)");
for &satellites in &[8_usize, 32, 64, 128] {
let tracks: Vec<Vec<OrbitSample>> = (0..satellites)
.map(|_| track(&satellite, 120, epoch))
.collect();
let finder = AccessFinder {
station: GroundPoint::new(Geodetic::new(0.0, 0.0, 0.0), 10.0_f64.to_radians()),
sample_step_seconds: TRACK_STEP_SECONDS,
crossing_tolerance_seconds: 0.01,
};
let iterations = if satellites <= 32 { 50 } else { 10 };
let durations = measure(iterations, || {
let mut passes = 0_usize;
for track in &tracks {
passes += finder.find(track).expect("the track is valid").count();
}
passes
});
row("access search", satellites, satellites as f64, &durations);
}
}
// ---- Three: topology and routing over N satellites. ------------------
// ---- 三:N 颗卫星上的拓扑与路由。-----------------------------
{
header("three: build topology over N satellites and route once");
for &satellites in &[8_usize, 24, 64, 120, 300, 600] {
// Positions spread on a ring so a fixed link range keeps a
// bounded number of neighbours, as a real shell does.
// 位置分布在一个环上,使固定的链路距离保持邻居数有界,正如真实壳层那样。
let positions: Vec<Vec3> = (0..satellites)
.map(|index| {
let angle = index as f64 * 0.4;
Vec3::new(
(BODY_RADIUS_KM + 550.0) * angle.cos(),
(BODY_RADIUS_KM + 550.0) * angle.sin(),
index as f64 * 20.0,
)
})
.collect();
let iterations = if satellites <= 24 {
50
} else if satellites <= 120 {
10
} else {
2
};
let durations = measure(iterations, || {
let mut topology = Topology::new(satellites);
for first in 0..satellites {
for second in 0..satellites {
if first == second {
continue;
}
if mutual_visibility(positions[first], positions[second], 4_500.0)
.is_some_and(|visible| visible)
{
let _ = topology.add_edge(first, second, 1.0);
}
}
}
let _ = apsis_routing::shortest_path(&topology, 0, satellites - 1);
topology.edge_count()
});
row(
"topology + route",
satellites,
satellites as f64,
&durations,
);
}
}
// ---- Four: plan N tasks on one resource. -----------------------------
// ---- 四:在一个资源上规划 N 个任务。--------------------------
{
header("four: plan N tasks on one resource, with a 50 long chain");
for &tasks in &[60_usize, 200, 500, 1_000, 2_000] {
let mut resource = Resource::new(ResourceId::new(0), 1.0);
resource
.set_windows(&[Window::new(0.0, tasks as f64 * 200.0 + 10_000.0)])
.expect("the window is usable");
let resources = ResourceSet::from_resources(vec![resource]).expect("distinct");
let set: Vec<Task> = (0..tasks)
.map(|index| {
let start = (index % 50) as f64 * 200.0;
Task::new(TaskId::new(index), 60.0)
.with_window(start, start + 180.0)
.with_priority(1.0 + (index % 7) as f64)
.try_require(ResourceId::new(0), 1.0)
.expect("the resource is new")
})
.collect();
let mut graph = PrecedenceGraph::new();
for index in 1..tasks.min(50) {
let _ = graph.add(TaskId::new(index - 1), TaskId::new(index));
}
let iterations = if tasks <= 200 { 20 } else { 5 };
let planner = Planner::new();
let durations = measure(iterations, || {
let result = planner.schedule_with_precedence(&set, &resources, &graph);
result.schedule.len()
});
row("plan tasks", tasks, tasks as f64, &durations);
}
}
println!();
println!("ns per unit is the figure to watch: a rising trend means super-linear growth");
}