aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cache/src/lib.rs13
-rw-r--r--database/src/kv/mod.rs27
-rw-r--r--database/src/lib.rs4
-rw-r--r--kv/src/lib.rs6
-rw-r--r--kv/src/rocksdb.rs35
-rw-r--r--locale/en.ini1
-rw-r--r--server/src/routes/admin/mod.rs19
-rw-r--r--server/src/routes/mod.rs6
-rw-r--r--ui/src/components/admin.rs6
9 files changed, 100 insertions, 17 deletions
diff --git a/cache/src/lib.rs b/cache/src/lib.rs
index 0a7a2f1..117b0d2 100644
--- a/cache/src/lib.rs
+++ b/cache/src/lib.rs
@@ -7,12 +7,14 @@ mod helper;
use anyhow::{Context, Result, anyhow};
pub use helper::{EscapeKey, HashKey};
+use humansize::{DECIMAL, SizeFormatter};
use jellykv::BlobStorage;
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{
any::Any,
collections::{BTreeMap, HashMap},
+ fmt::Write,
hash::{DefaultHasher, Hash, Hasher},
sync::{
Arc, LazyLock, Mutex, RwLock,
@@ -168,4 +170,15 @@ impl Cache {
humansize::format_size(reduction, humansize::DECIMAL)
);
}
+
+ pub fn debug_info(&self) -> Result<String> {
+ let mut o = String::new();
+ let msize = self.memory_cache_size.load(Ordering::Relaxed);
+ let mcount = self.memory_cache.read().unwrap().len();
+ let msize = SizeFormatter::new(msize, DECIMAL).to_string();
+ writeln!(o, "in-memory cache size: {msize:>12}")?;
+ writeln!(o, "in-memory cache obj count: {mcount:>12}")?;
+ write!(o, "{}", self.storage.debug_info()?)?;
+ Ok(o)
+ }
}
diff --git a/database/src/kv/mod.rs b/database/src/kv/mod.rs
index 784cbef..f391d31 100644
--- a/database/src/kv/mod.rs
+++ b/database/src/kv/mod.rs
@@ -41,6 +41,22 @@ impl<T: jellykv::Store> Database for T {
fn transaction(&self, f: &mut dyn FnMut(&mut dyn Transaction) -> Result<()>) -> Result<()> {
jellykv::Store::transaction(self, &mut |mut txn| f(&mut txn))
}
+ fn debug_info(&self) -> Result<String> {
+ let mut o = String::new();
+ self.transaction(&mut |txn| {
+ o = String::new();
+ let rc = read_counter(txn, &T_ROW_COUNTER.to_be_bytes(), 0)?;
+ writeln!(o, "row ounter: {rc}")?;
+ writeln!(o, "indices:")?;
+ for (is, ik) in list_indices(txn)? {
+ writeln!(o, "\tIS={is} IK={ik}")?;
+ }
+ Ok(())
+ })?;
+ writeln!(o)?;
+ write!(o, "{}", jellykv::Store::debug_info(self)?)?;
+ Ok(o)
+ }
}
impl Transaction for &mut dyn jellykv::Transaction {
@@ -125,17 +141,6 @@ impl Transaction for &mut dyn jellykv::Transaction {
}
Ok(total)
}
-
- fn debug_info(&self) -> Result<String> {
- let mut o = String::new();
- let rc = read_counter(*self, &T_ROW_COUNTER.to_be_bytes(), 0)?;
- writeln!(o, "Row Counter: {rc}")?;
- writeln!(o, "Indices:")?;
- for (is, ik) in list_indices(*self)? {
- writeln!(o, "\tIS={is} IK={ik}")?;
- }
- Ok(o)
- }
}
fn get_or_create_index(txn: &mut dyn jellykv::Transaction, ik: &IndexKey) -> Result<SubtreeNum> {
diff --git a/database/src/lib.rs b/database/src/lib.rs
index 465cb87..9bd4c06 100644
--- a/database/src/lib.rs
+++ b/database/src/lib.rs
@@ -17,6 +17,9 @@ pub type RowIter = Box<dyn Iterator<Item = Result<(RowNum, Vec<u8>)>>>;
pub trait Database: Send + Sync {
fn transaction(&self, f: &mut dyn FnMut(&mut dyn Transaction) -> Result<()>) -> Result<()>;
+ fn debug_info(&self) -> Result<String> {
+ Ok(String::new())
+ }
}
#[allow(clippy::type_complexity)]
@@ -31,7 +34,6 @@ pub trait Transaction {
) -> Result<Box<dyn Iterator<Item = Result<(RowNum, Vec<u8>)>> + 'a>>;
fn query_single(&mut self, query: Query) -> Result<Option<RowNum>>;
fn count(&mut self, query: Query) -> Result<u64>;
- fn debug_info(&self) -> Result<String>;
}
#[derive(Debug, Default, Clone, PartialEq)]
diff --git a/kv/src/lib.rs b/kv/src/lib.rs
index 83f2635..dd30460 100644
--- a/kv/src/lib.rs
+++ b/kv/src/lib.rs
@@ -20,6 +20,9 @@ use anyhow::Result;
pub trait Store: Send + Sync + 'static {
fn transaction(&self, f: &mut dyn FnMut(&mut dyn Transaction) -> Result<()>) -> Result<()>;
+ fn debug_info(&self) -> Result<String> {
+ Ok(String::new())
+ }
}
pub trait Transaction {
fn set(&mut self, key: &[u8], value: &[u8]) -> Result<()>;
@@ -35,4 +38,7 @@ pub trait Transaction {
pub trait BlobStorage: Send + Sync + 'static {
fn store(&self, key: &str, value: &[u8]) -> Result<()>;
fn read(&self, key: &str) -> Result<Option<Vec<u8>>>;
+ fn debug_info(&self) -> Result<String> {
+ Ok(String::new())
+ }
}
diff --git a/kv/src/rocksdb.rs b/kv/src/rocksdb.rs
index 683e399..04d7437 100644
--- a/kv/src/rocksdb.rs
+++ b/kv/src/rocksdb.rs
@@ -4,11 +4,17 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
+use crate::{BlobStorage, Store, Transaction};
use anyhow::Result;
+use humansize::{DECIMAL, SizeFormatter};
use rocksdb::{Direction, ErrorKind, IteratorMode, OptimisticTransactionDB};
-use std::path::Path;
+use std::{
+ path::Path,
+ sync::atomic::{AtomicU64, Ordering},
+};
-use crate::{BlobStorage, Store, Transaction};
+pub static NUM_TXN_COMMIT: AtomicU64 = AtomicU64::new(0);
+pub static NUM_TXN_ATTEMPT: AtomicU64 = AtomicU64::new(0);
pub fn new(path: &Path) -> Result<OptimisticTransactionDB> {
Ok(OptimisticTransactionDB::open_default(path)?)
@@ -18,13 +24,29 @@ impl Store for OptimisticTransactionDB {
fn transaction(&self, f: &mut dyn FnMut(&mut dyn Transaction) -> Result<()>) -> Result<()> {
loop {
let mut txn = self.transaction();
+ NUM_TXN_ATTEMPT.fetch_add(1, Ordering::Relaxed);
f(&mut txn)?;
match txn.commit() {
- Ok(()) => break Ok(()),
+ Ok(()) => break,
Err(e) if e.kind() == ErrorKind::Busy => continue,
Err(e) => return Err(e.into()),
}
}
+ NUM_TXN_COMMIT.fetch_add(1, Ordering::Relaxed);
+ Ok(())
+ }
+ fn debug_info(&self) -> Result<String> {
+ let size = self.get_approximate_sizes(&[rocksdb::Range::new(&[0x00], &[0xff])])[0];
+ let attempts = NUM_TXN_ATTEMPT.load(Ordering::Relaxed);
+ let commits = NUM_TXN_COMMIT.load(Ordering::Relaxed);
+ let retry_factor = attempts as f64 / commits as f64;
+ Ok(format!(
+ "transactions attempted: {attempts:>12}\n\
+ transactions commited: {commits:>12}\n\
+ transactions retry factor: {retry_factor:>12.03}\n\
+ approximate size on disk: {:>12}\n",
+ SizeFormatter::new(size, DECIMAL).to_string()
+ ))
}
}
@@ -67,4 +89,11 @@ impl BlobStorage for OptimisticTransactionDB {
fn read(&self, key: &str) -> Result<Option<Vec<u8>>> {
Ok(self.get(key)?)
}
+ fn debug_info(&self) -> Result<String> {
+ let size = self.get_approximate_sizes(&[rocksdb::Range::new(&[0x00], &[0xff])])[0];
+ Ok(format!(
+ "approximate size on disk: {:>12}\n",
+ SizeFormatter::new(size, DECIMAL).to_string()
+ ))
+ }
}
diff --git a/locale/en.ini b/locale/en.ini
index bf0e19c..1d0829a 100644
--- a/locale/en.ini
+++ b/locale/en.ini
@@ -184,6 +184,7 @@ prop.watched.watched=Watched
prop.vis.hidden=Hidden
prop.vis.reduced=Reduced visibility
+admin.debug=Debug Info
admin.dashboard=Admin Panel
admin.dashboard.import.inc=Start incremental import
admin.dashboard.import.full=Start full import
diff --git a/server/src/routes/admin/mod.rs b/server/src/routes/admin/mod.rs
index 6119b74..714b574 100644
--- a/server/src/routes/admin/mod.rs
+++ b/server/src/routes/admin/mod.rs
@@ -10,8 +10,9 @@ pub mod users;
use super::error::MyResult;
use crate::request_info::RequestInfo;
-use jellyui::components::admin::AdminDashboard;
+use jellyui::components::admin::{AdminDashboard, AdminDebug};
use rocket::{get, response::content::RawHtml};
+use std::fmt::Write;
#[get("/admin/dashboard")]
pub async fn r_admin_dashboard(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>> {
@@ -27,3 +28,19 @@ pub async fn r_admin_dashboard(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>>
ri: &ri.render_info(),
}))
}
+
+#[get("/admin/debug")]
+pub async fn r_admin_debug(ri: RequestInfo<'_>) -> MyResult<RawHtml<String>> {
+ ri.require_admin()?;
+ let mut o = String::new();
+ writeln!(o, "===== DATABASE =====")?;
+ write!(o, "{}", ri.state.database.debug_info()?)?;
+ writeln!(o)?;
+ writeln!(o, "===== CACHE =====")?;
+ write!(o, "{}", ri.state.cache.debug_info()?)?;
+
+ Ok(ri.respond_ui(&AdminDebug {
+ ri: &ri.render_info(),
+ debug_info: &o,
+ }))
+}
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index 05b8025..e93aac2 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -45,7 +45,10 @@ use self::{
stream::r_stream,
style::{r_assets_css, r_assets_font, r_assets_js, r_assets_js_map},
};
-use crate::{State, routes::search::r_search};
+use crate::{
+ State,
+ routes::{admin::r_admin_debug, search::r_search},
+};
use rocket::{
Build, Config, Rocket, catchers, fairing::AdHoc, fs::FileServer, http::Header, routes,
shield::Shield,
@@ -97,6 +100,7 @@ pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> {
r_account_settings_post,
r_account_settings,
r_admin_dashboard,
+ r_admin_debug,
r_admin_import_post,
r_admin_import_stream,
r_admin_import,
diff --git a/ui/src/components/admin.rs b/ui/src/components/admin.rs
index ddf49ca..d5828d2 100644
--- a/ui/src/components/admin.rs
+++ b/ui/src/components/admin.rs
@@ -17,6 +17,7 @@ use jellyui_locale::tr;
page!(AdminDashboard<'_>, |x| tr(x.ri.lang, "admin.dashboard"));
page!(AdminImport<'_>, |x| tr(x.ri.lang, "admin.import"));
page!(AdminUserList<'_>, |x| tr(x.ri.lang, "admin.users"));
+page!(AdminDebug<'_>, |x| tr(x.ri.lang, "admin.debug"));
page!(AdminUser<'_>, |x| x
.user
.get(USER_NAME)
@@ -35,6 +36,11 @@ markup::define!(
a[href=u_admin_users()] { h2 { @tr(ri.lang, "admin.users") }}
}
+ AdminDebug<'a>(ri: &'a RenderInfo<'a>, debug_info: &'a str) {
+ h1 { @tr(ri.lang, "admin.debug") }
+ pre { @debug_info }
+ }
+
AdminImport<'a>(ri: &'a RenderInfo<'a>, errors: &'a [&'a str], busy: bool) {
@if *busy {
h1 { @tr(ri.lang, "admin.import.running") }