• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vigna / webgraph-rs / 20166657030

12 Dec 2025 12:23PM UTC coverage: 62.07% (+0.02%) from 62.049%
20166657030

push

github

zommiommy
Add codes analysis comparison with both default codes and current codes

0 of 43 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

5446 of 8774 relevant lines covered (62.07%)

43389503.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.0
/cli/src/lib.rs
1
/*
2
 * SPDX-FileCopyrightText: 2023 Inria
3
 * SPDX-FileCopyrightText: 2023 Tommaso Fontana
4
 * SPDX-FileCopyrightText: 2025 Sebastiano Vigna
5
 *
6
 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
7
 */
8

9
#![doc = include_str!("../README.md")]
10
#![allow(clippy::type_complexity)]
11

12
use anyhow::{Context, Result, anyhow, bail, ensure};
13
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
14
use common_traits::{ToBytes, UnsignedInt};
15
use dsi_bitstream::dispatch::Codes;
16
use epserde::ser::Serialize;
17
use std::io::{BufWriter, Write};
18
use std::path::{Path, PathBuf};
19
use std::time::Duration;
20
use std::time::SystemTime;
21
use sux::bits::BitFieldVec;
22
use webgraph::prelude::CompFlags;
23
use webgraph::utils::{Granularity, MemoryUsage};
24

25
macro_rules! SEQ_PROC_WARN {
26
    () => {"Processing the graph sequentially: for parallel processing please build the Elias-Fano offsets list using 'webgraph build ef {}'"}
27
}
28

29
#[cfg(not(any(feature = "le_bins", feature = "be_bins")))]
30
compile_error!("At least one of the features `le_bins` or `be_bins` must be enabled.");
31

32
pub mod build_info {
33
    include!(concat!(env!("OUT_DIR"), "/built.rs"));
34

35
    pub fn version_string() -> String {
76✔
36
        format!(
76✔
37
            "{}
38
git info: {} {} {}
39
build info: built on {} for {} with {}",
40
            PKG_VERSION,
76✔
41
            GIT_VERSION.unwrap_or(""),
228✔
42
            GIT_COMMIT_HASH.unwrap_or(""),
228✔
43
            match GIT_DIRTY {
76✔
44
                None => "",
76✔
45
                Some(true) => "(dirty)",
×
46
                Some(false) => "(clean)",
×
47
            },
48
            BUILD_DATE,
49
            TARGET,
76✔
50
            RUSTC_VERSION
76✔
51
        )
52
    }
53
}
54

55
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
56
/// Enum for instantaneous codes.
57
///
58
/// It is used to implement [`ValueEnum`] here instead of in [`dsi_bitstream`].
59
pub enum PrivCode {
60
    Unary,
61
    Gamma,
62
    Delta,
63
    Zeta1,
64
    Zeta2,
65
    Zeta3,
66
    Zeta4,
67
    Zeta5,
68
    Zeta6,
69
    Zeta7,
70
    Pi1,
71
    Pi2,
72
    Pi3,
73
    Pi4,
74
}
75

76
impl From<PrivCode> for Codes {
77
    fn from(value: PrivCode) -> Self {
40✔
78
        match value {
40✔
79
            PrivCode::Unary => Codes::Unary,
8✔
80
            PrivCode::Gamma => Codes::Gamma,
24✔
81
            PrivCode::Delta => Codes::Delta,
×
82
            PrivCode::Zeta1 => Codes::Zeta(1),
×
83
            PrivCode::Zeta2 => Codes::Zeta(2),
×
84
            PrivCode::Zeta3 => Codes::Zeta(3),
8✔
85
            PrivCode::Zeta4 => Codes::Zeta(4),
×
86
            PrivCode::Zeta5 => Codes::Zeta(5),
×
87
            PrivCode::Zeta6 => Codes::Zeta(6),
×
88
            PrivCode::Zeta7 => Codes::Zeta(7),
×
89
            PrivCode::Pi1 => Codes::Pi(1),
×
90
            PrivCode::Pi2 => Codes::Pi(2),
×
91
            PrivCode::Pi3 => Codes::Pi(3),
×
92
            PrivCode::Pi4 => Codes::Pi(4),
×
93
        }
94
    }
95
}
96

97
#[derive(Args, Debug)]
98
/// Shared CLI arguments for reading files containing arcs.
99
pub struct ArcsArgs {
100
    #[arg(long, default_value_t = '#')]
101
    /// Ignore lines that start with this symbol.
102
    pub line_comment_symbol: char,
103

104
    #[arg(long, default_value_t = 0)]
105
    /// How many lines to skip, ignoring comment lines.
106
    pub lines_to_skip: usize,
107

108
    #[arg(long)]
109
    /// How many lines to parse, after skipping the first lines_to_skip and
110
    /// ignoring comment lines.
111
    pub max_arcs: Option<usize>,
112

113
    #[arg(long, default_value_t = '\t')]
114
    /// The column separator.
115
    pub separator: char,
116

117
    #[arg(long, default_value_t = 0)]
118
    /// The index of the column containing the source node of an arc.
119
    pub source_column: usize,
120

121
    #[arg(long, default_value_t = 1)]
122
    /// The index of the column containing the target node of an arc.
123
    pub target_column: usize,
124

125
    #[arg(long, default_value_t = false)]
126
    /// Source and destinations are not node identifiers starting from 0, but labels.
127
    pub labels: bool,
128
}
129

130
/// Parses the number of threads from a string.
131
///
132
/// This function is meant to be used with `#[arg(...,  value_parser =
133
/// num_threads_parser)]`.
134
pub fn num_threads_parser(arg: &str) -> Result<usize> {
12✔
135
    let num_threads = arg.parse::<usize>()?;
36✔
136
    ensure!(num_threads > 0, "Number of threads must be greater than 0");
24✔
137
    Ok(num_threads)
12✔
138
}
139

140
/// Shared CLI arguments for commands that specify a number of threads.
141
#[derive(Args, Debug)]
142
pub struct NumThreadsArg {
143
    #[arg(short = 'j', long, default_value_t = rayon::current_num_threads().max(1), value_parser = num_threads_parser)]
144
    /// The number of threads to use.
145
    pub num_threads: usize,
146
}
147

148
/// Shared CLI arguments for commands that specify a granularity.
149
#[derive(Args, Debug)]
150
pub struct GranularityArgs {
151
    #[arg(long, conflicts_with("node_granularity"))]
152
    /// The tentative number of arcs used to define the size of a parallel job
153
    /// (advanced option).
154
    pub arc_granularity: Option<u64>,
155

156
    #[arg(long, conflicts_with("arc_granularity"))]
157
    /// The tentative number of nodes used to define the size of a parallel job
158
    /// (advanced option).
159
    pub node_granularity: Option<usize>,
160
}
161

162
impl GranularityArgs {
163
    pub fn into_granularity(&self) -> Granularity {
4✔
164
        match (self.arc_granularity, self.node_granularity) {
8✔
165
            (Some(_), Some(_)) => unreachable!(),
166
            (Some(arc_granularity), None) => Granularity::Arcs(arc_granularity),
×
167
            (None, Some(node_granularity)) => Granularity::Nodes(node_granularity),
×
168
            (None, None) => Granularity::default(),
4✔
169
        }
170
    }
171
}
172

173
/// Shared CLI arguments for commands that specify a memory usage.
174
#[derive(Args, Debug)]
175
pub struct MemoryUsageArg {
176
    #[clap(short = 'm', long = "memory-usage", value_parser = memory_usage_parser, default_value = "50%")]
177
    /// The number of pairs to be used in batches.
178
    /// If the number ends with a `b` or `B` it is interpreted as a number of bytes, otherwise as a number of elements.
179
    /// You can use the SI and NIST multipliers k, M, G, T, P, ki, Mi, Gi, Ti, and Pi.
180
    /// You can also use a percentage of the available memory by appending a `%` to the number.
181
    pub memory_usage: MemoryUsage,
182
}
183

184
#[derive(Debug, Clone, Copy, ValueEnum)]
185
/// How to store vectors of floats.
186
pub enum FloatVectorFormat {
187
    /// Java-compatible format: a sequence of big-endian floats (32 or 64 bits).
188
    Java,
189
    /// A slice of floats (32 or 64 bits) serialized using ε-serde.
190
    Epserde,
191
    /// ASCII format, one float per line.
192
    Ascii,
193
    /// A JSON Array.
194
    Json,
195
}
196

197
impl FloatVectorFormat {
198
    /// Stores float values in the specified `path` using the format defined by
199
    /// `self`.
200
    ///
201
    /// If the result is a textual format, i.e., ASCII or JSON, `precision`
202
    /// will be used to truncate the float values to the specified number of
203
    /// decimal digits.
204
    pub fn store<F>(
×
205
        &self,
206
        path: impl AsRef<Path>,
207
        values: &[F],
208
        precision: Option<usize>,
209
    ) -> Result<()>
210
    where
211
        F: ToBytes + core::fmt::Display + epserde::ser::Serialize + Copy,
212
        for<'a> &'a [F]: epserde::ser::Serialize,
213
    {
214
        let precision = precision.unwrap_or(f64::DIGITS as usize);
×
215
        create_parent_dir(&path)?;
×
216
        let path_display = path.as_ref().display();
×
217
        let mut file = std::fs::File::create(&path)
×
218
            .with_context(|| format!("Could not create vector at {}", path_display))?;
×
219

220
        match self {
×
221
            FloatVectorFormat::Epserde => {
×
222
                log::info!("Storing in ε-serde format at {}", path_display);
×
223
                unsafe {
224
                    values
×
225
                        .serialize(&mut file)
×
226
                        .with_context(|| format!("Could not write vector to {}", path_display))
×
227
                }?;
228
            }
229
            FloatVectorFormat::Java => {
×
230
                log::info!("Storing in Java format at {}", path_display);
×
231
                for word in values.iter() {
×
232
                    file.write_all(word.to_be_bytes().as_ref())
×
233
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
234
                }
235
            }
236
            FloatVectorFormat::Ascii => {
×
237
                log::info!("Storing in ASCII format at {}", path_display);
×
238
                for word in values.iter() {
×
239
                    writeln!(file, "{word:.precision$}")
×
240
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
241
                }
242
            }
243
            FloatVectorFormat::Json => {
×
244
                log::info!("Storing in JSON format at {}", path_display);
×
245
                write!(file, "[")?;
×
246
                for word in values.iter().take(values.len().saturating_sub(2)) {
×
247
                    write!(file, "{word:.precision$}, ")
×
248
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
249
                }
250
                if let Some(last) = values.last() {
×
251
                    write!(file, "{last:.precision$}")
×
252
                        .with_context(|| format!("Could not write vector to {}", path_display))?;
×
253
                }
254
                write!(file, "]")?;
×
255
            }
256
        }
257

258
        Ok(())
×
259
    }
260
}
261

262
#[derive(Debug, Clone, Copy, ValueEnum)]
263
/// How to store vectors of integers.
264
pub enum IntVectorFormat {
265
    /// Java-compatible format: a sequence of big-endian longs (64 bits).
266
    Java,
267
    /// A slice of usize serialized using ε-serde.
268
    Epserde,
269
    /// A BitFieldVec stored using ε-serde. It stores each element using
270
    /// ⌊log₂(max)⌋ + 1 bits. It requires to allocate the `BitFieldVec` in RAM
271
    /// before serializing it.
272
    BitFieldVec,
273
    /// ASCII format, one integer per line.
274
    Ascii,
275
    /// A JSON Array.
276
    Json,
277
}
278

279
impl IntVectorFormat {
280
    /// Stores a vector of `u64` in the specified `path`` using the format defined by `self`.
281
    ///
282
    /// `max` is the maximum value of the vector. If it is not provided, it will
283
    /// be computed from the data.
284
    pub fn store(&self, path: impl AsRef<Path>, data: &[u64], max: Option<u64>) -> Result<()> {
×
285
        // Ensure the parent directory exists
286
        create_parent_dir(&path)?;
×
287

288
        let mut file = std::fs::File::create(&path)
×
289
            .with_context(|| format!("Could not create vector at {}", path.as_ref().display()))?;
×
290
        let mut buf = BufWriter::new(&mut file);
×
291

292
        debug_assert_eq!(
×
293
            max,
×
294
            max.map(|_| { data.iter().copied().max().unwrap_or(0) }),
×
295
            "The wrong maximum value was provided for the vector"
×
296
        );
297

298
        match self {
×
299
            IntVectorFormat::Epserde => {
×
300
                log::info!("Storing in epserde format at {}", path.as_ref().display());
×
301
                unsafe {
302
                    data.serialize(&mut buf).with_context(|| {
×
303
                        format!("Could not write vector to {}", path.as_ref().display())
×
304
                    })
305
                }?;
306
            }
307
            IntVectorFormat::BitFieldVec => {
×
308
                log::info!(
×
309
                    "Storing in BitFieldVec format at {}",
×
310
                    path.as_ref().display()
×
311
                );
312
                let max = max.unwrap_or_else(|| {
×
313
                    data.iter()
×
314
                        .copied()
×
315
                        .max()
×
316
                        .unwrap_or_else(|| panic!("Empty vector"))
×
317
                });
318
                let bit_width = max.len() as usize;
×
319
                log::info!("Using {} bits per element", bit_width);
×
320
                let mut bit_field_vec = <BitFieldVec<u64, _>>::with_capacity(bit_width, data.len());
×
321
                bit_field_vec.extend(data.iter().copied());
×
322
                unsafe {
323
                    bit_field_vec.store(&path).with_context(|| {
×
324
                        format!("Could not write vector to {}", path.as_ref().display())
×
325
                    })
326
                }?;
327
            }
328
            IntVectorFormat::Java => {
×
329
                log::info!("Storing in Java format at {}", path.as_ref().display());
×
330
                for word in data.iter() {
×
331
                    buf.write_all(&word.to_be_bytes()).with_context(|| {
×
332
                        format!("Could not write vector to {}", path.as_ref().display())
×
333
                    })?;
334
                }
335
            }
336
            IntVectorFormat::Ascii => {
×
337
                log::info!("Storing in ASCII format at {}", path.as_ref().display());
×
338
                for word in data.iter() {
×
339
                    writeln!(buf, "{}", word).with_context(|| {
×
340
                        format!("Could not write vector to {}", path.as_ref().display())
×
341
                    })?;
342
                }
343
            }
344
            IntVectorFormat::Json => {
×
345
                log::info!("Storing in JSON format at {}", path.as_ref().display());
×
346
                write!(buf, "[")?;
×
347
                for word in data.iter().take(data.len().saturating_sub(2)) {
×
348
                    write!(buf, "{}, ", word).with_context(|| {
×
349
                        format!("Could not write vector to {}", path.as_ref().display())
×
350
                    })?;
351
                }
352
                if let Some(last) = data.last() {
×
353
                    write!(buf, "{}", last).with_context(|| {
×
354
                        format!("Could not write vector to {}", path.as_ref().display())
×
355
                    })?;
356
                }
357
                write!(buf, "]")?;
×
358
            }
359
        };
360

361
        Ok(())
×
362
    }
363

364
    #[cfg(target_pointer_width = "64")]
365
    /// Stores a vector of `usize` in the specified `path` using the format defined by `self`.
366
    /// `max` is the maximum value of the vector, if it is not provided, it will
367
    /// be computed from the data.
368
    ///
369
    /// This helper method is available only on 64-bit architectures as Java's format
370
    /// uses of 64-bit integers.
371
    pub fn store_usizes(
×
372
        &self,
373
        path: impl AsRef<Path>,
374
        data: &[usize],
375
        max: Option<usize>,
376
    ) -> Result<()> {
377
        self.store(
×
378
            path,
×
379
            unsafe { core::mem::transmute::<&[usize], &[u64]>(data) },
×
380
            max.map(|x| x as u64),
×
381
        )
382
    }
383
}
384

385
/// Parses a batch size.
386
///
387
/// This function accepts either a number (possibly followed by a
388
/// SI or NIST multiplier k, M, G, T, P, ki, Mi, Gi, Ti, or Pi), or a percentage
389
/// (followed by a `%`) that is interpreted as a percentage of the core
390
/// memory. If the value ends with a `b` or `B` it is interpreted as a number of
391
/// bytes, otherwise as a number of elements.
392
pub fn memory_usage_parser(arg: &str) -> anyhow::Result<MemoryUsage> {
8✔
393
    const PREF_SYMS: [(&str, u64); 10] = [
394
        ("k", 1E3 as u64),
395
        ("m", 1E6 as u64),
396
        ("g", 1E9 as u64),
397
        ("t", 1E12 as u64),
398
        ("p", 1E15 as u64),
399
        ("ki", 1 << 10),
400
        ("mi", 1 << 20),
401
        ("gi", 1 << 30),
402
        ("ti", 1 << 40),
403
        ("pi", 1 << 50),
404
    ];
405
    let arg = arg.trim().to_ascii_lowercase();
24✔
406
    ensure!(!arg.is_empty(), "empty string");
16✔
407

408
    if arg.ends_with('%') {
8✔
409
        let perc = arg[..arg.len() - 1].parse::<f64>()?;
32✔
410
        ensure!((0.0..=100.0).contains(&perc), "percentage out of range");
32✔
411
        return Ok(MemoryUsage::from_perc(perc));
8✔
412
    }
413

414
    let num_digits = arg
×
415
        .chars()
416
        .take_while(|c| c.is_ascii_digit() || *c == '.')
×
417
        .count();
418

419
    let number = arg[..num_digits].parse::<f64>()?;
×
420
    let suffix = &arg[num_digits..].trim();
×
421

422
    let multiplier = PREF_SYMS
×
423
        .iter()
424
        .find(|(x, _)| suffix.starts_with(x))
×
425
        .map(|(_, m)| m)
×
426
        .ok_or(anyhow!("invalid prefix symbol {}", suffix))?;
×
427

428
    let value = (number * (*multiplier as f64)) as usize;
×
429
    ensure!(value > 0, "batch size must be greater than zero");
×
430

431
    if suffix.ends_with('b') {
×
432
        Ok(MemoryUsage::MemorySize(value))
×
433
    } else {
434
        Ok(MemoryUsage::BatchSize(value))
×
435
    }
436
}
437

438
#[derive(Args, Debug, Clone)]
439
/// Shared CLI arguments for compression.
440
pub struct CompressArgs {
441
    /// The endianness of the graph to write
442
    #[clap(short = 'E', long)]
443
    pub endianness: Option<String>,
444

445
    /// The compression windows
446
    #[clap(short = 'w', long, default_value_t = 7)]
447
    pub compression_window: usize,
448
    /// The minimum interval length
449
    #[clap(short = 'i', long, default_value_t = 4)]
450
    pub min_interval_length: usize,
451
    /// The maximum recursion depth for references (-1 for infinite recursion depth)
452
    #[clap(short = 'r', long, default_value_t = 3)]
453
    pub max_ref_count: isize,
454

455
    #[arg(value_enum)]
456
    #[clap(long, default_value = "gamma")]
457
    /// The code to use for the outdegree
458
    pub outdegrees: PrivCode,
459

460
    #[arg(value_enum)]
461
    #[clap(long, default_value = "unary")]
462
    /// The code to use for the reference offsets
463
    pub references: PrivCode,
464

465
    #[arg(value_enum)]
466
    #[clap(long, default_value = "gamma")]
467
    /// The code to use for the blocks
468
    pub blocks: PrivCode,
469

470
    #[arg(value_enum)]
471
    #[clap(long, default_value = "zeta3")]
472
    /// The code to use for the residuals
473
    pub residuals: PrivCode,
474

475
    /// Whether to use Zuckerli's reference selection algorithm. This slows down the compression
476
    /// process and requires more memory, but improves compression ratio and decoding speed.
477
    #[clap(long)]
478
    pub bvgraphz: bool,
479

480
    /// How many nodes to process in a chunk; the default (10000) is usually a good
481
    /// value.
482
    #[clap(long, default_value = "10000")]
483
    pub chunk_size: usize,
484
}
485

486
impl From<CompressArgs> for CompFlags {
487
    fn from(value: CompressArgs) -> Self {
8✔
488
        CompFlags {
489
            outdegrees: value.outdegrees.into(),
16✔
490
            references: value.references.into(),
16✔
491
            blocks: value.blocks.into(),
16✔
492
            intervals: PrivCode::Gamma.into(),
16✔
493
            residuals: value.residuals.into(),
16✔
494
            min_interval_length: value.min_interval_length,
8✔
495
            compression_window: value.compression_window,
8✔
496
            max_ref_count: match value.max_ref_count {
8✔
497
                -1 => usize::MAX,
498
                _ => value.max_ref_count as usize,
499
            },
500
        }
501
    }
502
}
503

504
/// Creates a [`ThreadPool`](rayon::ThreadPool) with the given number of threads.
505
pub fn get_thread_pool(num_threads: usize) -> rayon::ThreadPool {
12✔
506
    rayon::ThreadPoolBuilder::new()
12✔
507
        .num_threads(num_threads)
24✔
508
        .build()
509
        .expect("Failed to create thread pool")
510
}
511

512
/// Appends a string to the filename of a path.
513
///
514
/// # Panics
515
/// * Will panic if there is no filename.
516
/// * Will panic in test mode if the path has an extension.
517
pub fn append(path: impl AsRef<Path>, s: impl AsRef<str>) -> PathBuf {
×
518
    debug_assert!(path.as_ref().extension().is_none());
×
519
    let mut path_buf = path.as_ref().to_owned();
×
520
    let mut filename = path_buf.file_name().unwrap().to_owned();
×
521
    filename.push(s.as_ref());
×
522
    path_buf.push(filename);
×
523
    path_buf
×
524
}
525

526
/// Creates all parent directories of the given file path.
527
pub fn create_parent_dir(file_path: impl AsRef<Path>) -> Result<()> {
20✔
528
    // ensure that the dst directory exists
529
    if let Some(parent_dir) = file_path.as_ref().parent() {
40✔
530
        std::fs::create_dir_all(parent_dir).with_context(|| {
60✔
531
            format!(
×
532
                "Failed to create the directory {:?}",
×
533
                parent_dir.to_string_lossy()
×
534
            )
535
        })?;
536
    }
537
    Ok(())
20✔
538
}
539

540
/// Parse a duration from a string.
541
/// For compatibility with Java, if no suffix is given, it is assumed to be in milliseconds.
542
/// You can use suffixes, the available ones are:
543
/// - `s` for seconds
544
/// - `m` for minutes
545
/// - `h` for hours
546
/// - `d` for days
547
///
548
/// Example: `1d2h3m4s567` this is parsed as: 1 day, 2 hours, 3 minutes, 4 seconds, and 567 milliseconds.
549
fn parse_duration(value: &str) -> Result<Duration> {
×
550
    if value.is_empty() {
×
551
        bail!("Empty duration string, if you want every 0 milliseconds use `0`.");
×
552
    }
553
    let mut duration = Duration::from_secs(0);
×
554
    let mut acc = String::new();
×
555
    for c in value.chars() {
×
556
        if c.is_ascii_digit() {
×
557
            acc.push(c);
×
558
        } else if c.is_whitespace() {
×
559
            continue;
×
560
        } else {
561
            let dur = acc.parse::<u64>()?;
×
562
            match c {
×
563
                's' => duration += Duration::from_secs(dur),
×
564
                'm' => duration += Duration::from_secs(dur * 60),
×
565
                'h' => duration += Duration::from_secs(dur * 60 * 60),
×
566
                'd' => duration += Duration::from_secs(dur * 60 * 60 * 24),
×
567
                _ => return Err(anyhow!("Invalid duration suffix: {}", c)),
×
568
            }
569
            acc.clear();
×
570
        }
571
    }
572
    if !acc.is_empty() {
×
573
        let dur = acc.parse::<u64>()?;
×
574
        duration += Duration::from_millis(dur);
×
575
    }
576
    Ok(duration)
×
577
}
578

579
/// Initializes the `env_logger` logger with a custom format including
580
/// timestamps with elapsed time since initialization.
581
pub fn init_env_logger() -> Result<()> {
4✔
582
    use jiff::SpanRound;
583
    use jiff::fmt::friendly::{Designator, Spacing, SpanPrinter};
584

585
    let mut builder =
4✔
586
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"));
12✔
587

588
    let start = std::time::Instant::now();
8✔
589
    let printer = SpanPrinter::new()
8✔
590
        .spacing(Spacing::None)
8✔
591
        .designator(Designator::Compact);
8✔
592
    let span_round = SpanRound::new()
8✔
593
        .largest(jiff::Unit::Day)
8✔
594
        .smallest(jiff::Unit::Millisecond)
8✔
595
        .days_are_24_hours();
596

597
    builder.format(move |buf, record| {
3,812✔
598
        let Ok(ts) = jiff::Timestamp::try_from(SystemTime::now()) else {
7,608✔
599
            return Err(std::io::Error::other("Failed to get timestamp"));
×
600
        };
601
        let style = buf.default_level_style(record.level());
19,020✔
602
        let elapsed = start.elapsed();
11,412✔
603
        let span = jiff::Span::new()
7,608✔
604
            .seconds(elapsed.as_secs() as i64)
7,608✔
605
            .milliseconds(elapsed.subsec_millis() as i64);
7,608✔
606
        let span = span.round(span_round).expect("Failed to round span");
22,824✔
607
        writeln!(
3,804✔
608
            buf,
3,804✔
609
            "{} {} {style}{}{style:#} [{:?}] {} - {}",
610
            ts.strftime("%F %T%.3f"),
11,412✔
611
            printer.span_to_string(&span),
11,412✔
612
            record.level(),
7,608✔
613
            std::thread::current().id(),
7,608✔
614
            record.target(),
7,608✔
615
            record.args()
7,608✔
616
        )
617
    });
618
    builder.init();
8✔
619
    Ok(())
4✔
620
}
621

622
#[derive(Args, Debug)]
623
pub struct GlobalArgs {
624
    #[arg(long, value_parser = parse_duration, global=true, display_order = 1000)]
625
    /// How often to log progress. Default is 10s. You can use the suffixes `s`
626
    /// for seconds, `m` for minutes, `h` for hours, and `d` for days. If no
627
    /// suffix is provided it is assumed to be in milliseconds.
628
    /// Example: `1d2h3m4s567` is parsed as 1 day + 2 hours + 3 minutes + 4
629
    /// seconds + 567 milliseconds = 93784567 milliseconds.
630
    pub log_interval: Option<Duration>,
631
}
632

633
#[derive(Subcommand, Debug)]
634
pub enum SubCommands {
635
    #[command(subcommand)]
636
    Analyze(analyze::SubCommands),
637
    #[command(subcommand)]
638
    Bench(bench::SubCommands),
639
    #[command(subcommand)]
640
    Build(build::SubCommands),
641
    #[command(subcommand)]
642
    Check(check::SubCommands),
643
    #[command(subcommand)]
644
    From(from::SubCommands),
645
    #[command(subcommand)]
646
    Perm(perm::SubCommands),
647
    #[command(subcommand)]
648
    Run(run::SubCommands),
649
    #[command(subcommand)]
650
    To(to::SubCommands),
651
    #[command(subcommand)]
652
    Transform(transform::SubCommands),
653
}
654

655
#[derive(Parser, Debug)]
656
#[command(name = "webgraph", version=build_info::version_string())]
657
/// Webgraph tools to build, convert, modify, and analyze graphs.
658
#[doc = include_str!("common_env.txt")]
659
pub struct Cli {
660
    #[command(subcommand)]
661
    pub command: SubCommands,
662
    #[clap(flatten)]
663
    pub args: GlobalArgs,
664
}
665

666
pub mod dist;
667
pub mod sccs;
668

669
pub mod analyze;
670
pub mod bench;
671
pub mod build;
672
pub mod check;
673
pub mod from;
674
pub mod perm;
675
pub mod run;
676
pub mod to;
677
pub mod transform;
678

679
/// The entry point of the command-line interface.
680
pub fn cli_main<I, T>(args: I) -> Result<()>
12✔
681
where
682
    I: IntoIterator<Item = T>,
683
    T: Into<std::ffi::OsString> + Clone,
684
{
685
    let start = std::time::Instant::now();
24✔
686
    let cli = Cli::parse_from(args);
36✔
687
    match cli.command {
12✔
688
        SubCommands::Analyze(args) => {
×
689
            analyze::main(cli.args, args)?;
×
690
        }
691
        SubCommands::Bench(args) => {
×
692
            bench::main(cli.args, args)?;
×
693
        }
694
        SubCommands::Build(args) => {
7✔
695
            build::main(cli.args, args, Cli::command())?;
28✔
696
        }
697
        SubCommands::Check(args) => {
×
698
            check::main(cli.args, args)?;
×
699
        }
700
        SubCommands::From(args) => {
×
701
            from::main(cli.args, args)?;
×
702
        }
703
        SubCommands::Perm(args) => {
2✔
704
            perm::main(cli.args, args)?;
6✔
705
        }
706
        SubCommands::Run(args) => {
1✔
707
            run::main(cli.args, args)?;
3✔
708
        }
709
        SubCommands::To(args) => {
1✔
710
            to::main(cli.args, args)?;
3✔
711
        }
712
        SubCommands::Transform(args) => {
1✔
713
            transform::main(cli.args, args)?;
3✔
714
        }
715
    }
716

717
    log::info!(
12✔
718
        "The command took {}",
×
719
        pretty_print_elapsed(start.elapsed().as_secs_f64())
36✔
720
    );
721

722
    Ok(())
12✔
723
}
724

725
/// Pretty prints seconds in a humanly readable format.
726
fn pretty_print_elapsed(elapsed: f64) -> String {
48✔
727
    let mut result = String::new();
96✔
728
    let mut elapsed_seconds = elapsed as u64;
96✔
729
    let weeks = elapsed_seconds / (60 * 60 * 24 * 7);
96✔
730
    elapsed_seconds %= 60 * 60 * 24 * 7;
48✔
731
    let days = elapsed_seconds / (60 * 60 * 24);
96✔
732
    elapsed_seconds %= 60 * 60 * 24;
48✔
733
    let hours = elapsed_seconds / (60 * 60);
96✔
734
    elapsed_seconds %= 60 * 60;
48✔
735
    let minutes = elapsed_seconds / 60;
96✔
736
    //elapsed_seconds %= 60;
737

738
    match weeks {
48✔
739
        0 => {}
48✔
740
        1 => result.push_str("1 week "),
×
741
        _ => result.push_str(&format!("{} weeks ", weeks)),
×
742
    }
743
    match days {
48✔
744
        0 => {}
48✔
745
        1 => result.push_str("1 day "),
×
746
        _ => result.push_str(&format!("{} days ", days)),
×
747
    }
748
    match hours {
48✔
749
        0 => {}
48✔
750
        1 => result.push_str("1 hour "),
×
751
        _ => result.push_str(&format!("{} hours ", hours)),
×
752
    }
753
    match minutes {
48✔
754
        0 => {}
44✔
UNCOV
755
        1 => result.push_str("1 minute "),
×
756
        _ => result.push_str(&format!("{} minutes ", minutes)),
12✔
757
    }
758

759
    result.push_str(&format!("{:.3} seconds ({}s)", elapsed % 60.0, elapsed));
144✔
760
    result
48✔
761
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc