Skip to main content

radicle_cli/commands/
stats.rs

1mod args;
2
3use std::path::Path;
4
5use localtime::LocalDuration;
6use localtime::LocalTime;
7use radicle::git;
8use radicle::issue::cache::Issues as _;
9use radicle::node::address;
10use radicle::node::routing;
11use radicle::patch::cache::Patches as _;
12use radicle::storage::{ReadRepository, ReadStorage, WriteRepository};
13use radicle_term::Element;
14use serde::Serialize;
15
16use crate::terminal as term;
17
18pub use args::Args;
19
20#[derive(Default, Serialize)]
21#[serde(rename_all = "camelCase")]
22struct NodeStats {
23    all: usize,
24    public_daily: usize,
25    online_daily: usize,
26    online_weekly: usize,
27    seeding_weekly: usize,
28}
29
30#[derive(Default, Serialize)]
31#[serde(rename_all = "camelCase")]
32struct LocalStats {
33    repos: usize,
34    issues: usize,
35    patches: usize,
36    pushes: usize,
37    forks: usize,
38}
39
40#[derive(Default, Serialize)]
41#[serde(rename_all = "camelCase")]
42struct RepoStats {
43    unique: usize,
44    replicas: usize,
45}
46
47#[derive(Default, Serialize)]
48#[serde(rename_all = "camelCase")]
49struct Stats {
50    local: LocalStats,
51    repos: RepoStats,
52    nodes: NodeStats,
53}
54
55pub fn run(_args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
56    let profile = ctx.profile()?;
57    let storage = &profile.storage;
58    let mut stats = Stats::default();
59
60    for repo in storage.repositories()? {
61        let repo = storage.repository(repo.rid)?;
62        let issues = term::cob::issues(&profile, &repo)?.counts()?;
63        let patches = term::cob::patches(&profile, &repo)?.counts()?;
64
65        stats.local.issues += issues.total();
66        stats.local.patches += patches.total();
67        stats.local.repos += 1;
68
69        for remote in repo.remote_ids()? {
70            let remote = remote?;
71            let sigrefs = repo.reference_oid(&remote, &git::refs::storage::SIGREFS_BRANCH)?;
72            let mut walk = repo.raw().revwalk()?;
73            walk.push(sigrefs.into())?;
74
75            stats.local.pushes += walk.count();
76            stats.local.forks += 1;
77        }
78    }
79
80    let now = LocalTime::now();
81    let db = profile.database()?;
82    stats.nodes.all = address::Store::nodes(&db)?;
83    stats.repos.replicas = routing::Store::len(&db)?;
84
85    {
86        let row = db
87            .db
88            .prepare("SELECT COUNT(DISTINCT repo) FROM routing")?
89            // SAFETY: `COUNT` always returns a row.
90            .into_iter()
91            .next()
92            .unwrap()?;
93        let count = row.try_read::<i64, _>(0)? as usize;
94
95        stats.repos.unique = count;
96    }
97
98    {
99        let since = now - LocalDuration::from_mins(60 * 24); // 1 day.
100        let mut stmt = db.db.prepare(
101            "SELECT COUNT(DISTINCT node) FROM announcements WHERE timestamp >= ?1 AND timestamp < ?2",
102        )?;
103        stmt.bind((1, since.as_millis() as i64))?;
104        stmt.bind((2, now.as_millis() as i64))?;
105
106        // SAFETY: `COUNT` always returns a row.
107        let row = stmt.iter().next().unwrap()?;
108        stats.nodes.online_daily = row.try_read::<i64, _>(0)? as usize;
109
110        let since = now - LocalDuration::from_mins(60 * 24 * 7); // 1 week.
111        stmt.reset()?;
112        stmt.bind((1, since.as_millis() as i64))?;
113        stmt.bind((2, now.as_millis() as i64))?;
114
115        let row = stmt.iter().next().unwrap()?;
116        stats.nodes.online_weekly = row.try_read::<i64, _>(0)? as usize;
117    }
118
119    {
120        let since = now - LocalDuration::from_mins(60 * 24); // 1 day.
121        let mut stmt = db.db.prepare(
122            "SELECT COUNT(DISTINCT ann.node) FROM announcements as ann
123             JOIN addresses AS addr
124             ON ann.node == addr.node
125             WHERE ann.timestamp >= ?1 AND ann.timestamp < ?2",
126        )?;
127        stmt.bind((1, since.as_millis() as i64))?;
128        stmt.bind((2, now.as_millis() as i64))?;
129
130        let row = stmt
131            .into_iter()
132            .next()
133            // SAFETY: `COUNT` always returns a row.
134            .unwrap()?;
135        let count = row.try_read::<i64, _>(0)? as usize;
136
137        stats.nodes.public_daily = count;
138    }
139
140    {
141        let since = now - LocalDuration::from_mins(60 * 24 * 7); // 1 week.
142        let mut stmt = db.db.prepare(
143            "SELECT COUNT(DISTINCT node) FROM routing
144             WHERE timestamp >= ?1 AND timestamp < ?2",
145        )?;
146        stmt.bind((1, since.as_millis() as i64))?;
147        stmt.bind((2, now.as_millis() as i64))?;
148
149        let row = stmt
150            .into_iter()
151            .next()
152            // SAFETY: `COUNT` always returns a row.
153            .unwrap()?;
154        let count = row.try_read::<i64, _>(0)? as usize;
155
156        stats.nodes.seeding_weekly = count;
157    }
158
159    let output = term::json::to_pretty(&stats, Path::new("stats.json"))?;
160    output.print();
161
162    Ok(())
163}