radicle_cli/commands/
stats.rs1mod 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 .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); 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 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); 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); 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 .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); 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 .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}