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