Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Add `rad stats` command
cloudhead committed 2 years ago
commit ae981ded6ed2ed2cdba34c8603714782667f18a3
parent a60afc8350b1ae538cf87450fab20b1813e5af2f
7 files changed +211 -0
modified radicle-cli/src/commands.rs
@@ -42,6 +42,8 @@ pub mod rad_remote;
pub mod rad_seed;
#[path = "commands/self.rs"]
pub mod rad_self;
+
#[path = "commands/stats.rs"]
+
pub mod rad_stats;
#[path = "commands/sync.rs"]
pub mod rad_sync;
#[path = "commands/unfollow.rs"]
modified radicle-cli/src/commands/help.rs
@@ -32,6 +32,7 @@ const COMMANDS: &[Help] = &[
    rad_follow::HELP,
    rad_unfollow::HELP,
    rad_remote::HELP,
+
    rad_stats::HELP,
    rad_sync::HELP,
];

added radicle-cli/src/commands/stats.rs
@@ -0,0 +1,175 @@
+
use std::ffi::OsString;
+
use std::path::Path;
+

+
use localtime::LocalDuration;
+
use localtime::LocalTime;
+
use radicle::cob::issue;
+
use radicle::cob::patch;
+
use radicle::git;
+
use radicle::node::address;
+
use radicle::node::routing;
+
use radicle::storage::{ReadRepository, ReadStorage, WriteRepository};
+
use radicle_term::Element;
+
use serde::Serialize;
+

+
use crate::terminal as term;
+
use crate::terminal::args::{Args, Error, Help};
+

+
pub const HELP: Help = Help {
+
    name: "stats",
+
    description: "Displays aggregated repository and node metrics",
+
    version: env!("CARGO_PKG_VERSION"),
+
    usage: r#"
+
Usage
+

+
    rad stats [<option>...]
+

+
Options
+

+
    --help       Print help
+
"#,
+
};
+

+
#[derive(Default, Serialize)]
+
struct NodeStats {
+
    all: usize,
+
    public: usize,
+
    online: usize,
+
    seeding: usize,
+
}
+

+
#[derive(Default, Serialize)]
+
struct LocalStats {
+
    repos: usize,
+
    issues: usize,
+
    patches: usize,
+
    pushes: usize,
+
    forks: usize,
+
}
+

+
#[derive(Default, Serialize)]
+
struct RepoStats {
+
    unique: usize,
+
    replicas: usize,
+
}
+

+
#[derive(Default, Serialize)]
+
struct Stats {
+
    local: LocalStats,
+
    repos: RepoStats,
+
    nodes: NodeStats,
+
}
+

+
#[derive(Default, Debug, Eq, PartialEq)]
+
pub struct Options {}
+

+
impl Args for Options {
+
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
+
        use lexopt::prelude::*;
+

+
        let mut parser = lexopt::Parser::from_args(args);
+

+
        #[allow(clippy::never_loop)]
+
        while let Some(arg) = parser.next()? {
+
            match arg {
+
                Long("help") | Short('h') => {
+
                    return Err(Error::Help.into());
+
                }
+
                _ => return Err(anyhow::anyhow!(arg.unexpected())),
+
            }
+
        }
+

+
        Ok((Options {}, vec![]))
+
    }
+
}
+

+
pub fn run(_options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
+
    let profile = ctx.profile()?;
+
    let storage = &profile.storage;
+
    let mut stats = Stats::default();
+

+
    for repo in storage.repositories()? {
+
        let repo = storage.repository(repo.rid)?;
+
        let issues = issue::Issues::open(&repo)?.counts()?;
+
        let patches = patch::Patches::open(&repo)?.counts()?;
+

+
        stats.local.issues += issues.total();
+
        stats.local.patches += patches.total();
+
        stats.local.repos += 1;
+

+
        for remote in repo.remote_ids()? {
+
            let remote = remote?;
+
            let sigrefs = repo.reference_oid(&remote, &git::refs::storage::SIGREFS_BRANCH)?;
+
            let mut walk = repo.raw().revwalk()?;
+
            walk.push(*sigrefs)?;
+

+
            stats.local.pushes += walk.count();
+
            stats.local.forks += 1;
+
        }
+
    }
+

+
    let db = profile.database()?;
+
    stats.nodes.all = address::Store::nodes(&db)?;
+
    stats.repos.replicas = routing::Store::len(&db)?;
+

+
    {
+
        let row = db
+
            .db
+
            .prepare("SELECT COUNT(DISTINCT repo) FROM routing")?
+
            // SAFETY: `COUNT` always returns a row.
+
            .into_iter()
+
            .next()
+
            .unwrap()?;
+
        let count = row.read::<i64, _>(0) as usize;
+

+
        stats.repos.unique = count;
+
    }
+

+
    {
+
        let now = LocalTime::now();
+
        let since = now - LocalDuration::from_mins(60 * 24); // 1 day.
+
        let mut stmt = db.db.prepare(
+
            "SELECT COUNT(DISTINCT node) FROM announcements WHERE timestamp >= ?1 and timestamp < ?2",
+
        )?;
+

+
        stmt.bind((1, since.as_millis() as i64))?;
+
        stmt.bind((2, now.as_millis() as i64))?;
+

+
        // SAFETY: `COUNT` always returns a row.
+
        let row = stmt.into_iter().next().unwrap()?;
+
        let count = row.read::<i64, _>(0) as usize;
+

+
        stats.nodes.online = count;
+
    }
+

+
    {
+
        let row = db
+
            .db
+
            .prepare("SELECT COUNT(DISTINCT node) FROM addresses")?
+
            .into_iter()
+
            .next()
+
            // SAFETY: `COUNT` always returns a row.
+
            .unwrap()?;
+
        let count = row.read::<i64, _>(0) as usize;
+

+
        stats.nodes.public = count;
+
    }
+

+
    {
+
        let row = db
+
            .db
+
            .prepare("SELECT COUNT(DISTINCT node) FROM routing")?
+
            .into_iter()
+
            .next()
+
            // SAFETY: `COUNT` always returns a row.
+
            .unwrap()?;
+
        let count = row.read::<i64, _>(0) as usize;
+

+
        stats.nodes.seeding = count;
+
    }
+

+
    let output = term::json::to_pretty(&stats, Path::new("stats.json"))?;
+
    output.print();
+

+
    Ok(())
+
}
modified radicle-cli/src/main.rs
@@ -261,6 +261,11 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
            rad_remote::run,
            args.to_vec(),
        ),
+
        "stats" => term::run_command_args::<rad_stats::Options, _>(
+
            rad_stats::HELP,
+
            rad_stats::run,
+
            args.to_vec(),
+
        ),
        "watch" => term::run_command_args::<rad_watch::Options, _>(
            rad_watch::HELP,
            rad_watch::run,
modified radicle/src/cob/issue.rs
@@ -681,6 +681,13 @@ pub struct IssueCounts {
    pub closed: usize,
}

+
impl IssueCounts {
+
    /// Total count.
+
    pub fn total(&self) -> usize {
+
        self.open + self.closed
+
    }
+
}
+

impl<'a, R: WriteRepository> Issues<'a, R>
where
    R: ReadRepository + cob::Store,
modified radicle/src/cob/patch.rs
@@ -2171,6 +2171,13 @@ pub struct PatchCounts {
    pub merged: usize,
}

+
impl PatchCounts {
+
    /// Total count.
+
    pub fn total(&self) -> usize {
+
        self.open + self.draft + self.archived + self.merged
+
    }
+
}
+

pub struct Patches<'a, R> {
    raw: store::Store<'a, Patch, R>,
}
modified radicle/src/node/address/store.rs
@@ -57,6 +57,8 @@ pub trait Store {
    fn remove(&mut self, id: &NodeId) -> Result<bool, Error>;
    /// Returns the number of addresses.
    fn len(&self) -> Result<usize, Error>;
+
    /// Return the number of nodes.
+
    fn nodes(&self) -> Result<usize, Error>;
    /// Returns true if there are no addresses.
    fn is_empty(&self) -> Result<bool, Error> {
        self.len().map(|l| l == 0)
@@ -149,6 +151,18 @@ impl Store for Database {
        Ok(count)
    }

+
    fn nodes(&self) -> Result<usize, Error> {
+
        let row = self
+
            .db
+
            .prepare("SELECT COUNT(*) FROM nodes")?
+
            .into_iter()
+
            .next()
+
            .ok_or(Error::NoRows)??;
+
        let count = row.read::<i64, _>(0) as usize;
+

+
        Ok(count)
+
    }
+

    fn insert(
        &mut self,
        node: &NodeId,