Add /memuse command

This commit is contained in:
2023-11-05 20:47:05 +01:00
parent 224e63c9da
commit 55e814e955
11 changed files with 323 additions and 10 deletions

View File

@ -46,6 +46,7 @@ lazy_static = "1.4.0"
lender = "0.2.1"
libc = "0.2.148"
log = "0.4.20"
memuse = "0.2.1"
nonempty = { version = "0.8.1", features = ["serialize"] }
signal-hook = "0.3.17"
smallvec = "1.11.1"

View File

@ -19,6 +19,7 @@ use std::sync::Arc;
use std::sync::RwLock;
use matrix_sdk::async_trait;
use memuse::DynamicUsage;
use ratatui::text::Text;
use tokio::sync::mpsc::UnboundedReceiver;
use tracing_error::ErrorLayer;
@ -44,6 +45,15 @@ impl LogBuffer {
}
}
impl DynamicUsage for LogBuffer {
fn dynamic_usage(&self) -> usize {
self.lines.dynamic_usage()
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
self.lines.dynamic_usage_bounds()
}
}
#[async_trait]
impl Buffer for LogBuffer {
fn short_name(&self) -> String {

View File

@ -39,7 +39,7 @@ pub struct BufferItem<'buf> {
}
#[async_trait]
pub trait Buffer: Send + Sync {
pub trait Buffer: Send + Sync + memuse::DynamicUsage {
/// A short human-readable name for the room, eg. to show in compact buflist
fn short_name(&self) -> String;
/// If this is a room buffer, returns the associated room id.
@ -53,6 +53,13 @@ pub trait Buffer: Send + Sync {
///
/// This should return immediately, not waiting for anything to be loaded.
fn request_back_pagination(&self, num: u16) {}
fn dynamic_usage(&self) -> usize {
memuse::DynamicUsage::dynamic_usage(self)
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
memuse::DynamicUsage::dynamic_usage_bounds(self)
}
}
pub struct Buffers {

View File

@ -30,6 +30,7 @@ use matrix_sdk_ui::timeline::{
BackPaginationStatus, PaginationOptions, RoomExt, Timeline, TimelineItem, TimelineItemKind,
VirtualTimelineItem,
};
use memuse::DynamicUsage;
use ratatui::text::Text;
use smallvec::SmallVec;
@ -44,6 +45,31 @@ pub enum OwnedBufferItemContent {
Empty,
}
impl DynamicUsage for OwnedBufferItemContent {
fn dynamic_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ match self {
OwnedBufferItemContent::Text(s) | OwnedBufferItemContent::Divider(s) => s.dynamic_usage(),
OwnedBufferItemContent::Empty => 0,
}
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
let (min, max) = match self {
OwnedBufferItemContent::Text(s) | OwnedBufferItemContent::Divider(s) => {
s.dynamic_usage_bounds()
},
OwnedBufferItemContent::Empty => (0, Some(0)),
};
match max {
Some(max) => (
min + std::mem::size_of::<Self>(),
Some(max + std::mem::size_of::<Self>()),
),
None => (min + std::mem::size_of::<Self>(), None),
}
}
}
/// Like `format!()` but returns OwnedBufferItemContent::Text
macro_rules! text {
($($tokens:tt)*) => {
@ -61,6 +87,29 @@ pub struct SingleClientRoomBuffer {
back_pagination_request: AtomicU16,
}
impl DynamicUsage for SingleClientRoomBuffer {
fn dynamic_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ self
.items
.iter()
.map(|(content, prerender)| content.dynamic_usage() + prerender.dynamic_usage())
.sum::<usize>()
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
let self_usage = std::mem::size_of::<Self>();
crate::utils::sum_memory_bounds(
self_usage,
Some(self_usage),
self
.items
.iter()
.map(|item| item.0.dynamic_usage_bounds())
.chain(self.items.iter().map(|item| item.1.dynamic_usage_bounds())),
)
}
}
impl SingleClientRoomBuffer {
async fn poll_updates(&mut self) {
let back_pagination_request = self.back_pagination_request.swap(0, Ordering::Relaxed);
@ -271,10 +320,23 @@ pub struct RoomBuffer {
buffers: SmallVec<[SingleClientRoomBuffer; 1]>,
}
fn f(b: RoomBuffer) {
g(b);
impl DynamicUsage for RoomBuffer {
fn dynamic_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ self
.buffers
.iter()
.map(|buf| buf.dynamic_usage())
.sum::<usize>()
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
crate::utils::sum_memory_bounds(
std::mem::size_of::<Self>(),
Some(std::mem::size_of::<Self>()),
self.buffers.iter().map(|buf| buf.dynamic_usage_bounds()),
)
}
}
fn g<B: Send>(b: B) {}
impl RoomBuffer {
pub async fn new(initial_client: Client, room_id: OwnedRoomId) -> Result<Self> {

View File

@ -22,7 +22,8 @@ use tokio::sync::mpsc;
use crate::action::Action;
pub type CommandHandler = Box<dyn Fn(&str, &str, &mpsc::UnboundedSender<Action>) -> Result<()>>;
pub type CommandHandler =
Box<dyn Fn(&str, &str, &crate::App, &mpsc::UnboundedSender<Action>) -> Result<()>>;
pub trait RataCommand {
fn name(&self) -> String;
@ -74,7 +75,7 @@ pub fn run_command(
};
match app.commands.0.get(&command_name.to_ascii_lowercase()) {
Some(command) => command.handler()(command_name, args, action_tx),
Some(command) => command.handler()(command_name, args, app, action_tx),
None => bail!("Unknown command /{}", command_name),
}
}

View File

@ -22,10 +22,10 @@ use crate::commands::{CommandHandler, RataCommand};
use crate::plugins::{Plugin, PluginGetter, PrePlugin};
inventory::submit! {
PluginGetter(get_core)
PluginGetter(get_plugin)
}
fn get_core() -> Result<Box<dyn PrePlugin>> {
fn get_plugin() -> Result<Box<dyn PrePlugin>> {
Ok(Box::new(Core {}))
}
@ -33,7 +33,7 @@ struct Core {}
impl PrePlugin for Core {
fn name(&self) -> String {
"core".to_owned()
"Core".to_owned()
}
fn help(&self) -> String {
@ -88,7 +88,7 @@ impl RataCommand for ActionCommand {
}
fn handler(&self) -> CommandHandler {
let action = self.action.clone();
Box::new(move |_name, _args, action_tx| {
Box::new(move |_name, _args, _app, action_tx| {
action_tx
.send(action.clone())
.context("Could not queue action")

120
src/plugins/memuse.rs Normal file
View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2023 Valentin Lorentz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use color_eyre::eyre::{Result, WrapErr};
use itertools::Itertools;
use tokio::sync::mpsc;
use crate::action::Action;
use crate::commands::{CommandHandler, RataCommand};
use crate::plugins::{Plugin, PluginGetter, PrePlugin};
inventory::submit! {
PluginGetter(get_plugin)
}
fn get_plugin() -> Result<Box<dyn PrePlugin>> {
Ok(Box::new(Memuse {}))
}
struct Memuse {}
impl PrePlugin for Memuse {
fn name(&self) -> String {
"Memuse".to_owned()
}
fn help(&self) -> String {
"estimates memory usage".to_owned()
}
fn load(self: Box<Self>, app: &mut crate::App) -> Result<Box<dyn Plugin>> {
app.commands.register(Box::new(MemuseCommand {}));
Ok(self)
}
}
impl Plugin for Memuse {}
struct MemuseCommand {}
impl RataCommand for MemuseCommand {
fn name(&self) -> String {
"memuse".to_owned()
}
fn help(&self) -> String {
"returns memory usage estimates".to_owned()
}
fn handler(&self) -> CommandHandler {
Box::new(move |_name, _args, app, action_tx| {
/*
let client_usage = app
.clients
.iter()
.map(|client| format_mem_usage(client.user_id().map(|user_id| user_id.as_str()).unwrap_or("<INITIALIZING>"), client))
.join("\n");*/
let client_usage = "todo";
let buffer_usage = app
.buffers
.iter()
.map(|buffer| format_mem_usage2(&buffer.short_name(), buffer))
.join("\n");
tracing::info!(
"Client memory usage (matrix-sdk):\n{}\nBuffer memory usage:\n{}",
client_usage,
buffer_usage
);
Ok(())
})
}
}
fn format_mem_usage<T: memuse::DynamicUsage>(name: &str, value: T) -> String {
match value.dynamic_usage_bounds() {
(min, Some(max)) => format!(
"* {}: {} to {} bytes, approximately {}",
name,
min,
max,
value.dynamic_usage()
),
(min, None) => format!(
"* {}: approximately {} bytes but no less than {}",
name,
min,
value.dynamic_usage()
),
}
}
/// Boo trait objects
fn format_mem_usage2(name: &str, value: &dyn crate::buffers::Buffer) -> String {
match crate::buffers::Buffer::dynamic_usage_bounds(value) {
(min, Some(max)) => format!(
"* {}: {} to {} bytes, approximately {}",
name,
min,
max,
crate::buffers::Buffer::dynamic_usage(value)
),
(min, None) => format!(
"* {}: approximately {} bytes but no less than {}",
name,
min,
crate::buffers::Buffer::dynamic_usage(value)
),
}
}

View File

@ -19,6 +19,7 @@ use color_eyre::eyre::{Result, WrapErr};
use crate::App;
pub mod core;
pub mod memuse;
/// A [`Plugin`] that is not initialized yet
pub trait PrePlugin {

View File

@ -148,3 +148,26 @@ Config directory: {config_dir_path}
Data directory: {data_dir_path}"
)
}
pub fn sum_memory_bounds(
min: usize,
max: Option<usize>,
bounds: impl Iterator<Item = (usize, Option<usize>)>,
) -> (usize, Option<usize>) {
let mut total_min = min;
let mut total_max = max;
for item in bounds {
match item {
(min, Some(max)) => {
total_min += min;
total_max.as_mut().map(|total_max| *total_max += max);
},
(min, None) => {
total_min += min;
total_max = None;
},
}
}
(total_min, total_max)
}

66
src/widgets/divider.rs Normal file
View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2023 Valentin Lorentz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::widgets::OverlappableWidget;
use ratatui::prelude::*;
use ratatui::widgets::*;
pub struct Divider<'a> {
pub line_type: BorderType,
pub text: Paragraph<'a>,
}
impl<'a> Divider<'a> {
pub fn new(text: Paragraph<'a>) -> Self {
Divider {
line_type: BorderType::default(),
text,
}
}
}
impl<'a> OverlappableWidget for Divider<'a> {
fn height(&self, width: u16) -> u64 {
1
}
fn render_overlap(
self,
area: ratatui::layout::Rect,
buf: &mut ratatui::buffer::Buffer,
) -> (u16, u16) {
if area.height == 0 {
return (area.width, 0);
}
let symbol = BorderType::border_symbols(self.line_type).horizontal_top;
for x in 0..area.width {
buf
.get_mut(area.x + x, area.y + area.height - 1)
.set_symbol(symbol);
}
self.text.render(
Rect {
y: area.y + area.height - 1,
height: 1,
..area
},
buf,
);
(area.width, 1)
}
}

View File

@ -16,6 +16,8 @@
use std::sync::{Arc, Mutex};
use memuse::DynamicUsage;
/// Storage for the result of pre-computations by the UI, with interior mutability
#[derive(Clone)]
pub(crate) struct PrerenderInner {
@ -44,3 +46,23 @@ impl Prerender {
self.0.lock().unwrap().as_ref().map(|inner| inner.key)
}
}
impl DynamicUsage for Prerender {
fn dynamic_usage(&self) -> usize {
std::mem::size_of::<Self>()
+ match self.0.lock().unwrap().as_ref() {
Some(PrerenderInner {
key: _,
value: PrerenderValue::Rendered(buf),
}) => {
(buf.area().height as usize)
* (buf.area().width as usize)
* std::mem::size_of::<ratatui::buffer::Cell>()
},
_ => 0,
}
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
(self.dynamic_usage(), Some(self.dynamic_usage()))
}
}