use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::path::Path;
thread_local! {
static THREAD_ALLOCATED: Cell<u64> = const { Cell::new(0) };
}
struct CountingAlloc;
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = System.alloc(layout);
if !p.is_null() {
let _ =
THREAD_ALLOCATED.try_with(|c| c.set(c.get().wrapping_add(layout.size() as u64)));
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout);
}
}
#[global_allocator]
static GLOBAL: CountingAlloc = CountingAlloc;
fn allocated_by(f: impl FnOnce()) -> u64 {
let before = THREAD_ALLOCATED.with(|c| c.get());
f();
THREAD_ALLOCATED.with(|c| c.get()).wrapping_sub(before)
}
#[test]
#[ignore]
fn mem_calibration() {
use crate::config::Config;
eprintln!("size_of App = {}", crate::app::sizeof_app());
eprintln!("size_of TabState = {}", crate::app::sizeof_tabstate());
eprintln!("size_of Config = {}", std::mem::size_of::<Config>());
let dir = std::env::temp_dir().join("konoma_mem_calib_md");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut md = String::from("# Top\n\n");
for i in 0..300 {
md.push_str(&format!(
"## H {i}\n\nBody *x* `c` [l](https://e/{i}).\n\n- [ ] t\n\n"
));
}
std::fs::write(dir.join("big.md"), &md).unwrap();
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let mut app = crate::app::App::new(dir.clone(), Config::default()).unwrap();
app.selected = app
.entries
.iter()
.position(|e| e.path.ends_with("big.md"))
.unwrap();
app.tree_activate().unwrap();
let mut term = Terminal::new(TestBackend::new(80, 30)).unwrap();
let first_draw = allocated_by(|| {
term.draw(|f| crate::ui::render(f, &mut app)).unwrap(); });
let one_scroll = allocated_by(|| {
app.preview_scroll(3);
term.draw(|f| crate::ui::render(f, &mut app)).unwrap();
});
eprintln!("md first-draw alloc= {first_draw}");
eprintln!(
"md one-scroll alloc= {one_scroll} (ratio {:.2})",
one_scroll as f64 / first_draw as f64
);
let open_alloc = |lines: usize| -> (u64, u64) {
let mut body = String::new();
for i in 0..lines {
body.push_str(&format!("line {i} with some content here\n"));
}
let f = dir.join(format!("huge_{lines}.txt"));
std::fs::write(&f, &body).unwrap();
let mut a = crate::app::App::new(dir.clone(), Config::default()).unwrap();
a.selected = a.entries.iter().position(|e| e.path == f).unwrap();
let mut t2 = Terminal::new(TestBackend::new(80, 30)).unwrap();
let alloc = allocated_by(|| {
a.tree_activate().unwrap();
t2.draw(|f| crate::ui::render(f, &mut a)).unwrap();
});
(body.len() as u64, alloc)
};
let (sz1, al1) = open_alloc(50_000);
let (sz2, al2) = open_alloc(200_000); eprintln!("huge 50k : size={sz1} open_alloc={al1}");
eprintln!(
"huge 200k: size={sz2} open_alloc={al2} (alloc ratio {:.2}, size ratio {:.2})",
al2 as f64 / al1 as f64,
sz2 as f64 / sz1 as f64
);
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(test)]
fn mem_preview(
dir: &Path,
name: &str,
) -> (
crate::app::App,
ratatui::Terminal<ratatui::backend::TestBackend>,
) {
use crate::config::Config;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let mut app = crate::app::App::new(dir.to_path_buf(), Config::default()).unwrap();
app.selected = app
.entries
.iter()
.position(|e| e.path.ends_with(name))
.unwrap();
app.tree_activate().unwrap();
let mut term = Terminal::new(TestBackend::new(80, 30)).unwrap();
term.draw(|f| crate::ui::render(f, &mut app)).unwrap();
(app, term)
}
#[test]
fn md_scroll_reuses_cache_not_redecorate() {
let dir = std::env::temp_dir().join("konoma_mem_md_reuse");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let mut md = String::from("# Top\n\n");
for i in 0..300 {
md.push_str(&format!(
"## H {i}\n\nBody *x* `c` [l](https://e/{i}).\n\n- [ ] t\n\n"
));
}
std::fs::write(dir.join("big.md"), &md).unwrap();
use crate::config::Config;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
let mut app = crate::app::App::new(dir.clone(), Config::default()).unwrap();
app.selected = app
.entries
.iter()
.position(|e| e.path.ends_with("big.md"))
.unwrap();
app.tree_activate().unwrap();
let mut term = Terminal::new(TestBackend::new(80, 30)).unwrap();
let first_draw = allocated_by(|| {
term.draw(|f| crate::ui::render(f, &mut app)).unwrap();
});
let one_scroll = allocated_by(|| {
app.preview_scroll(3);
term.draw(|f| crate::ui::render(f, &mut app)).unwrap();
});
assert!(
one_scroll.saturating_mul(20) < first_draw,
"スクロールが装飾キャッシュを再利用していない(1スクロール {one_scroll} が初回装飾 {first_draw} に近い=毎フレーム再装飾?)"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn windowed_preview_does_not_scale_with_file_size() {
let dir = std::env::temp_dir().join("konoma_mem_window_scale");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let write_lines = |name: &str, n: usize| {
let mut b = String::new();
for i in 0..n {
b.push_str(&format!("line {i} with some content here\n"));
}
std::fs::write(dir.join(name), b).unwrap();
};
write_lines("warm.txt", 500);
write_lines("small.txt", 5_000);
write_lines("large.txt", 80_000);
let _ = mem_preview(&dir, "warm.txt");
let small_alloc = allocated_by(|| {
let _ = mem_preview(&dir, "small.txt");
});
let large_alloc = allocated_by(|| {
let _ = mem_preview(&dir, "large.txt");
});
assert!(
large_alloc < small_alloc.saturating_mul(3),
"windowed プレビューがファイルサイズに比例している(16x のファイルで alloc {large_alloc} が小 {small_alloc} の 3x 超=全文読み込み?)"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn per_tab_and_app_structs_stay_small() {
let app = crate::app::sizeof_app();
let tab = crate::app::sizeof_tabstate();
assert!(app < 16_384, "App が肥大した: {app} バイト");
assert!(
tab < 4_096,
"TabState が肥大した(タブ数ぶん倍増する): {tab} バイト"
);
}
#[test]
fn table_rows_are_capped() {
let dir = std::env::temp_dir().join("konoma_mem_table_cap");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("huge.csv");
let mut b = String::from("a,b,c\n");
for i in 0..(crate::preview::table::MAX_ROWS + 5_000) {
b.push_str(&format!("{i},x,y\n"));
}
std::fs::write(&f, b).unwrap();
let td = crate::preview::table::parse(&f, b',').unwrap();
assert!(
td.nrows() <= crate::preview::table::MAX_ROWS,
"行数が MAX_ROWS を超えた: {}",
td.nrows()
);
assert!(td.truncated, "上限超過は truncated=true で通知");
std::fs::remove_dir_all(&dir).ok();
}