diff options
| -rw-r--r-- | cache/src/lib.rs | 13 | ||||
| -rw-r--r-- | database/src/kv/mod.rs | 27 | ||||
| -rw-r--r-- | database/src/lib.rs | 4 | ||||
| -rw-r--r-- | kv/src/lib.rs | 6 | ||||
| -rw-r--r-- | kv/src/rocksdb.rs | 35 | ||||
| -rw-r--r-- | locale/en.ini | 1 | ||||
| -rw-r--r-- | server/src/routes/admin/mod.rs | 19 | ||||
| -rw-r--r-- | server/src/routes/mod.rs | 6 | ||||
| -rw-r--r-- | ui/src/components/admin.rs | 6 |
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") } |