commonware-runtime 2026.10.0

Execute asynchronous tasks with a configurable scheduler.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
//! CLI and configuration types.

use clap::{CommandFactory, Parser, ValueEnum, builder::Styles, error::ErrorKind, value_parser};
use std::{env, fmt, path::PathBuf, time::Duration};

/// Runtime whose storage backend is benchmarked.
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum Backend {
    /// The Tokio runtime.
    #[value(name = "tokio")]
    Tokio,
    /// The io_uring runtime (Linux only).
    #[value(name = "iouring")]
    Iouring,
}

impl Backend {
    /// The runtime's default worker thread count.
    fn default_worker_threads(self) -> usize {
        match self {
            Self::Tokio => commonware_runtime::tokio::Config::default().worker_threads(),
            #[cfg(target_os = "linux")]
            Self::Iouring => commonware_runtime::iouring::Config::default().worker_threads(),
            #[cfg(not(target_os = "linux"))]
            Self::Iouring => unreachable!("validation rejects io_uring off Linux"),
        }
    }
}

/// Benchmark workload to execute.
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum Workload {
    /// Sequential reads over a fixed-size file.
    #[value(name = "read_seq")]
    ReadSeq,
    /// Uniform random reads over a fixed-size file.
    #[value(name = "read_rand")]
    ReadRand,
    /// Sequential non-overlapping overwrites over a fixed-size file.
    #[value(name = "write_seq")]
    WriteSeq,
    /// Uniform random in-place overwrites over a fixed-size file.
    #[value(name = "write_rand")]
    WriteRand,
    /// Monotonic append writes to a growing file.
    #[value(name = "write_append")]
    WriteAppend,
    /// Sequential durable positioned writes over a fixed-size file.
    #[value(name = "write_sync")]
    WriteSync,
    /// One append writer plus many random readers of the visible prefix.
    #[value(name = "read_write_append")]
    ReadWriteAppend,
}

impl Workload {
    /// Whether the workload benchmarks writes.
    pub const fn has_writes(self) -> bool {
        matches!(
            self,
            Self::WriteSeq
                | Self::WriteRand
                | Self::WriteAppend
                | Self::WriteSync
                | Self::ReadWriteAppend
        )
    }

    /// Whether the workload benchmarks reads.
    pub const fn has_reads(self) -> bool {
        matches!(self, Self::ReadSeq | Self::ReadRand | Self::ReadWriteAppend)
    }
}

/// Read cache preparation mode.
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum CacheMode {
    /// Best-effort warming by touching the file before timing.
    #[value(name = "warm")]
    Warm,
    /// Best-effort eviction with `posix_fadvise(..., DONTNEED)` before timing.
    /// Only the first pass through the file is truly cold: subsequent passes
    /// read from cache. Use a large enough file or short enough duration that
    /// the timed phase does not complete a full pass.
    #[value(name = "cold")]
    Cold,
}

/// Write payload layout.
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum WriteShape {
    /// Single contiguous buffer per write.
    #[value(name = "contiguous")]
    Contiguous,
    /// Four-buffer vectored write per operation.
    #[value(name = "vectored")]
    Vectored,
}

/// Durable write implementation for the `write_sync` workload.
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum SyncMethod {
    /// Call `write_at`, then call `sync`.
    #[value(name = "write_then_sync")]
    WriteThenSync,
    /// Call `write_at` with `WriteOptions::SYNC`.
    #[value(name = "write_and_sync")]
    WriteAndSync,
}

/// Write durability policy.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SyncMode {
    /// Flush once at the end of the timed phase.
    End,
    /// Flush every `N` writes in each writer stream.
    Every(u64),
}

/// Output format.
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum OutputFormat {
    /// Human-readable multi-line report.
    #[value(name = "human")]
    Human,
    /// Single JSON object.
    #[value(name = "json")]
    Json,
}

macro_rules! display_value_enum {
    ($($ty:ty),+ $(,)?) => {$(
        impl fmt::Display for $ty {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                f.write_str(
                    self.to_possible_value()
                        .expect("all variants have clap names")
                        .get_name(),
                )
            }
        }
    )+};
}

display_value_enum!(
    Backend,
    Workload,
    CacheMode,
    WriteShape,
    SyncMethod,
    OutputFormat
);

impl fmt::Display for SyncMode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::End => f.write_str("end"),
            Self::Every(count) => write!(f, "{count}"),
        }
    }
}

/// Parsed and validated benchmark configuration.
#[derive(Clone, Debug, Parser)]
#[command(
    name = "storage_bench",
    about = "Benchmark the runtime storage backend",
    styles = Styles::styled(),
)]
pub struct Config {
    /// Runtime whose storage backend is benchmarked.
    #[arg(long, value_enum, default_value = "tokio")]
    pub backend: Backend,

    /// Workload to execute.
    #[arg(long, value_enum)]
    pub workload: Workload,

    /// Timed run duration in seconds.
    #[arg(long, default_value_t = 30, value_parser = value_parser!(u64).range(1..))]
    duration: u64,

    /// Read or write size in bytes. Accepts suffixes like 64K, 4M, or 1G.
    #[arg(long, default_value = "4096", value_parser = parse_byte_size_usize)]
    pub io_size: usize,

    /// Concurrent I/O streams, each run as its own task.
    #[arg(long, default_value_t = 1, value_parser = value_parser!(usize))]
    pub inflight: usize,

    /// Worker thread count for the benchmark runtime. Defaults to the
    /// runtime's own setting.
    #[arg(long, value_parser = value_parser!(usize))]
    worker_threads: Option<usize>,

    /// io_uring ring capacity, rounded up to a power of two. Defaults to 1024.
    /// Requires `--backend iouring`.
    #[arg(long, value_parser = value_parser!(u32).range(1..=32768))]
    pub ring_size: Option<u32>,

    /// Poll interval at which a busy worker looks at the global queue first.
    /// Defaults to the runtime's own setting.
    #[arg(long, value_parser = value_parser!(u32))]
    pub global_queue_interval: Option<u32>,

    /// Initial fixed-size file length. Accepts suffixes like 64K, 4M, or 1G.
    #[arg(long, value_parser = parse_byte_size)]
    pub file_size: Option<u64>,

    /// Existing parent directory under which a unique benchmark directory is created.
    #[arg(long, default_value_os_t = default_root())]
    pub root: PathBuf,

    /// Best-effort cache preparation for read-heavy workloads.
    #[arg(long, value_enum)]
    pub cache: Option<CacheMode>,

    /// Write payload layout for write-heavy workloads.
    #[arg(long, value_enum, default_value = "contiguous")]
    pub write_shape: WriteShape,

    /// Durable write method for the write_sync workload.
    #[arg(long, value_enum, default_value = "write_then_sync")]
    pub sync_method: SyncMethod,

    /// Durability cadence: `end` or a positive integer per writer stream.
    #[arg(long = "sync-every", default_value = "end", value_parser = parse_sync_mode)]
    pub sync_mode: SyncMode,

    /// Deterministic seed for payloads and random offsets.
    #[arg(long, default_value_t = 0)]
    pub seed: u64,

    /// Report format.
    #[arg(long, value_enum, default_value = "human")]
    pub output: OutputFormat,
}

impl Config {
    /// Parse CLI arguments and validate cross-field constraints.
    ///
    /// On parse or validation failure the error is printed directly (with
    /// proper formatting and color) and the process exits, matching clap's
    /// default behavior.
    pub fn parse() -> Self {
        // Filter out `--bench` which cargo passes to harness-less bench targets.
        let args = std::env::args_os().filter(|a| a != "--bench");
        let cfg = <Self as Parser>::try_parse_from(args).unwrap_or_else(|err| err.exit());
        if let Err(msg) = cfg.validate() {
            Self::command()
                .error(ErrorKind::ValueValidation, msg)
                .exit();
        }
        cfg
    }

    /// Timed run duration.
    pub const fn duration(&self) -> Duration {
        Duration::from_secs(self.duration)
    }

    /// Initial file size (panics if not set; only call after validation).
    pub const fn file_size(&self) -> u64 {
        self.file_size
            .expect("validated configuration must include --file-size")
    }

    /// Worker thread count after applying the runtime's default.
    pub fn worker_threads(&self) -> usize {
        self.worker_threads
            .unwrap_or_else(|| self.backend.default_worker_threads())
    }

    /// Ring capacity after applying the default and rounding, absent for Tokio.
    pub fn effective_ring_size(&self) -> Option<u32> {
        (self.backend == Backend::Iouring)
            .then(|| self.ring_size.unwrap_or(1024).next_power_of_two())
    }

    fn validate(&self) -> Result<(), String> {
        if self.backend == Backend::Iouring && !cfg!(target_os = "linux") {
            return Err("--backend iouring is only supported on Linux".into());
        }
        if self.inflight == 0 {
            return Err("--inflight must be greater than zero".into());
        }
        if self.worker_threads == Some(0) {
            return Err("--worker-threads must be greater than zero".into());
        }
        if self.backend != Backend::Iouring && self.ring_size.is_some() {
            return Err("--ring-size requires --backend iouring".into());
        }
        if self.global_queue_interval == Some(0) {
            return Err("--global-queue-interval must be greater than zero".into());
        }

        match self.workload {
            Workload::WriteAppend => {
                if self.file_size.is_some() {
                    return Err("--file-size is not used by write_append".into());
                }
                if self.inflight != 1 {
                    return Err("write_append only supports --inflight 1".into());
                }
                if self.cache.is_some() {
                    return Err("--cache is only valid for read-heavy workloads".into());
                }
            }
            _ => {
                let file_size = self
                    .file_size
                    .ok_or_else(|| "--file-size is required for this workload".to_string())?;
                let io_size = self.io_size as u64;
                if file_size < io_size {
                    return Err("--file-size must be at least --io-size".into());
                }
                if !file_size.is_multiple_of(io_size) {
                    return Err("--file-size must be a multiple of --io-size".into());
                }
                if matches!(
                    self.workload,
                    Workload::WriteSeq | Workload::WriteRand | Workload::WriteSync
                ) {
                    let total_blocks = file_size / io_size;
                    if total_blocks < self.inflight as u64 {
                        return Err(
                            "write_seq, write_rand, and write_sync require at least one non-overlapping block per worker"
                                .into(),
                        );
                    }
                    if self.workload == Workload::WriteSync
                        && !total_blocks.is_multiple_of(self.inflight as u64)
                    {
                        return Err(
                            "write_sync requires --file-size / --io-size to be divisible by --inflight"
                                .into(),
                        );
                    }
                }
            }
        }

        if self.workload.has_reads() {
            if self.cache.is_none() {
                return Err("--cache is required for read-heavy workloads".into());
            }
        } else if self.cache.is_some() {
            return Err("--cache is only valid for read-heavy workloads".into());
        }
        if matches!(self.cache, Some(CacheMode::Cold)) && !cfg!(target_os = "linux") {
            return Err("--cache cold is only supported on Linux".into());
        }

        if !self.workload.has_writes() {
            if self.write_shape != WriteShape::Contiguous {
                return Err("--write-shape is only valid for write-heavy workloads".into());
            }
            if self.sync_method != SyncMethod::WriteThenSync {
                return Err("--sync-method is only valid for write_sync".into());
            }
            if self.sync_mode != SyncMode::End {
                return Err("--sync-every is only valid for write-heavy workloads".into());
            }
        } else if self.workload == Workload::WriteSync {
            if self.sync_mode != SyncMode::End {
                return Err("--sync-every is not used by write_sync".into());
            }
        } else if self.sync_method != SyncMethod::WriteThenSync {
            return Err("--sync-method is only valid for write_sync".into());
        }

        Ok(())
    }
}

fn default_root() -> PathBuf {
    env::temp_dir()
}

fn parse_sync_mode(value: &str) -> Result<SyncMode, String> {
    if value == "end" {
        return Ok(SyncMode::End);
    }
    let count = value
        .parse::<u64>()
        .map_err(|err| format!("invalid value for --sync-every: {err}"))?;
    if count == 0 {
        return Err("--sync-every must be `end` or a positive integer".into());
    }
    Ok(SyncMode::Every(count))
}

fn parse_byte_size_usize(value: &str) -> Result<usize, String> {
    let bytes = parse_byte_size(value)?;
    usize::try_from(bytes).map_err(|_| format!("value is too large for this platform: {value}"))
}

/// Supported suffixes use binary multiples: `K`, `M`, `G`, `T`, `KB`, `MB`,
/// `GB`, `TB`, and the explicit `KiB`, `MiB`, `GiB`, `TiB` forms.
fn parse_byte_size(value: &str) -> Result<u64, String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return Err("size value cannot be empty".into());
    }

    let split = trimmed
        .find(|c: char| !(c.is_ascii_digit() || c == '_'))
        .unwrap_or(trimmed.len());
    let (number_part, suffix_part) = trimmed.split_at(split);
    if number_part.is_empty() {
        return Err(format!("invalid size value: {value}"));
    }

    let number = number_part.replace('_', "");
    let number = number
        .parse::<u64>()
        .map_err(|err| format!("invalid size value `{value}`: {err}"))?;

    let multiplier = match suffix_part.trim().to_ascii_uppercase().as_str() {
        "" | "B" => 1,
        "K" | "KB" | "KIB" => 1024,
        "M" | "MB" | "MIB" => 1024_u64.pow(2),
        "G" | "GB" | "GIB" => 1024_u64.pow(3),
        "T" | "TB" | "TIB" => 1024_u64.pow(4),
        suffix => {
            return Err(format!(
                "invalid size suffix `{suffix}` in `{value}`; expected K, M, G, T, KB, MB, GB, TB, KiB, MiB, GiB, or TiB"
            ));
        }
    };

    let result = number
        .checked_mul(multiplier)
        .ok_or_else(|| format!("size value is too large: {value}"))?;
    if result == 0 {
        return Err("size value must be greater than zero".into());
    }
    Ok(result)
}