diff --git a/Cargo.lock b/Cargo.lock index c9c73ac..e5aaa39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "cpuprofiler" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f8479dbcfd2bbaa0c0c26779b913052b375981cdf533091f2127ea3d42e52b" +dependencies = [ + "error-chain", + "lazy_static", + "pkg-config", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -519,6 +530,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + [[package]] name = "fdeflate" version = "0.3.4" @@ -1470,7 +1491,7 @@ dependencies = [ [[package]] name = "ratatui-image" version = "1.0.0" -source = "git+https://github.com/itsjunetime/ratatui-image.git?branch=vb64_on_personal#88876c749898cce7801379345eb4e0bc69111b8a" +source = "git+https://github.com/itsjunetime/ratatui-image.git?branch=vb64_on_personal#8d368ca87c9b817811ba47d04d188a334e5da5a6" dependencies = [ "base64 0.22.1", "dyn-clone", @@ -1478,6 +1499,7 @@ dependencies = [ "image", "rand", "ratatui", + "rayon", "rustix", "vb64", ] @@ -1809,6 +1831,7 @@ version = "0.1.0" dependencies = [ "cairo-rs", "console-subscriber", + "cpuprofiler", "criterion", "crossterm", "flume", diff --git a/Cargo.toml b/Cargo.toml index 7358c45..3614568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ tracing = ["tokio/tracing", "dep:console-subscriber"] [dev-dependencies] criterion = { version = "0.5.1", features = ["async_tokio"] } +cpuprofiler = "0.0.4" [[bench]] name = "rendering" diff --git a/benches/rendering.rs b/benches/rendering.rs index 4a98081..b9792e4 100644 --- a/benches/rendering.rs +++ b/benches/rendering.rs @@ -1,7 +1,21 @@ mod utils; -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use utils::{render_doc, render_first_page}; +use std::{ + hint::black_box, + path::Path, + time::{SystemTime, UNIX_EPOCH} +}; + +use criterion::{criterion_group, criterion_main, profiler::Profiler, BenchmarkId, Criterion}; +use futures_util::StreamExt; +use tdf::{ + converter::{ConvertedPage, ConverterMsg}, + renderer::{fill_default, PageInfo, RenderInfo} +}; +use utils::{ + handle_converter_msg, handle_renderer_msg, render_doc, start_all_rendering, + start_converting_loop, start_rendering_loop, RenderState +}; const FILES: [&str; 2] = [ "benches/example_dictionary.pdf", @@ -18,7 +32,7 @@ fn render_full(c: &mut Criterion) { } fn render_to_first_page(c: &mut Criterion) { - for file in FILES { + for file in &FILES[..1] { c.bench_with_input( BenchmarkId::new("render_first_page", file), &file, @@ -30,9 +44,135 @@ fn render_to_first_page(c: &mut Criterion) { } } +fn only_converting(c: &mut Criterion) { + for file in FILES { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let all_rendered = runtime.block_on(render_all_files(file)); + + c.bench_with_input( + BenchmarkId::new("only_converting", file), + &(all_rendered, file), + |b, (rendered, _)| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| convert_all_files(rendered.clone())) + } + ); + } +} + +pub async fn render_first_page(path: impl AsRef) { + let RenderState { + mut from_render_rx, + mut from_converter_rx, + mut pages, + mut to_converter_tx, + to_render_tx + } = start_all_rendering(path); + + // we only want to render until the first page is ready to be printed + while pages.is_empty() { + tokio::select! { + Some(renderer_msg) = from_render_rx.next() => { + handle_renderer_msg(renderer_msg, &mut pages, &mut to_converter_tx); + }, + Some(converter_msg) = from_converter_rx.next() => { + handle_converter_msg(converter_msg, &mut pages, &mut to_converter_tx); + } + } + } + + black_box(pages); + // we want to make sure this is kept around until the end of this function, or else the other + // thread will see that this is disconnected and think that we're done communicating with them + drop(to_render_tx); +} + +async fn render_all_files(path: &'static str) -> Vec { + let (mut from_render_rx, to_render_tx) = start_rendering_loop(path); + let mut pages = Vec::>::new(); + + while let Some(info) = from_render_rx.next().await { + match info.expect("Renderer ran into an error while rendering") { + RenderInfo::NumPages(num) => fill_default(&mut pages, num), + RenderInfo::Page(page) => { + let num = page.page; + pages[num] = Some(page); + } + }; + + if pages.iter().all(|p| p.is_some()) { + break; + } + } + + drop(to_render_tx); + pages.into_iter().flatten().collect() +} + +async fn convert_all_files(files: Vec) { + let num_files = files.len(); + let (mut from_converter_rx, to_converter_tx) = start_converting_loop(num_files); + + to_converter_tx + .send(ConverterMsg::NumPages(num_files)) + .unwrap(); + + let mut converted = Vec::>::new(); + fill_default(&mut converted, num_files); + + for page in files { + to_converter_tx.send(ConverterMsg::AddImg(page)).unwrap(); + + if !from_converter_rx.is_empty() { + let page = from_converter_rx + .next() + .await + .expect("Converter ended stream before expected") + .expect("Converter ran into an error while converting page"); + + let num = page.num; + converted[num] = Some(page); + } + } + + while converted.iter().any(|p| p.is_none()) { + let page = from_converter_rx + .next() + .await + .expect("Converted ended stream before expected") + .expect("Converted ran into an error while converting page"); + + let num = page.num; + converted[num] = Some(page); + } + + drop(to_converter_tx); + black_box(converted); +} + +struct CpuProfiler; + +impl Profiler for CpuProfiler { + fn start_profiling(&mut self, benchmark_id: &str, _: &std::path::Path) { + let file = format!( + "./{}-{}.profile", + benchmark_id.replace("/", "-"), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() + ); + + cpuprofiler::PROFILER.lock().unwrap().start(file).unwrap() + } + fn stop_profiling(&mut self, _: &str, _: &std::path::Path) { + cpuprofiler::PROFILER.lock().unwrap().stop().unwrap(); + } +} + criterion_group!( name = benches; - config = Criterion::default().sample_size(40); - targets = render_full, render_to_first_page + config = Criterion::default().sample_size(40).with_profiler(CpuProfiler); + targets = render_full, render_to_first_page, only_converting ); criterion_main!(benches); diff --git a/benches/utils.rs b/benches/utils.rs index c15d86f..b14e27f 100644 --- a/benches/utils.rs +++ b/benches/utils.rs @@ -10,7 +10,7 @@ use tdf::{ renderer::{fill_default, start_rendering, RenderError, RenderInfo, RenderNotif} }; -fn handle_renderer_msg( +pub fn handle_renderer_msg( msg: Result, pages: &mut Vec>, to_converter_tx: &mut Sender @@ -25,7 +25,7 @@ fn handle_renderer_msg( } } -fn handle_converter_msg( +pub fn handle_converter_msg( msg: Result, pages: &mut [Option], to_converter_tx: &mut Sender @@ -44,43 +44,39 @@ fn handle_converter_msg( .unwrap(); } -struct RenderState { - from_render_rx: RecvStream<'static, Result>, - from_converter_rx: RecvStream<'static, Result>, - pages: Vec>, - to_converter_tx: Sender, - to_render_tx: Sender +pub struct RenderState { + pub from_render_rx: RecvStream<'static, Result>, + pub from_converter_rx: RecvStream<'static, Result>, + pub pages: Vec>, + pub to_converter_tx: Sender, + pub to_render_tx: Sender } -fn start_all_rendering(path: impl AsRef) -> RenderState { +const FONT_SIZE: (u16, u16) = (8, 14); + +pub fn start_rendering_loop( + path: impl AsRef +) -> ( + RecvStream<'static, Result>, + Sender +) { let pathbuf = path.as_ref().canonicalize().unwrap(); let str_path = format!("file://{}", pathbuf.into_os_string().to_string_lossy()); let (to_render_tx, from_main_rx) = unbounded(); let (to_main_tx, from_render_rx) = unbounded(); - let font_size = (8, 14); let (columns, rows) = (60, 180); let size = WindowSize { columns, rows, - height: rows * font_size.1, - width: columns * font_size.0 + height: rows * FONT_SIZE.1, + width: columns * FONT_SIZE.0 }; std::thread::spawn(move || start_rendering(str_path, to_main_tx, from_main_rx, size)); - let (to_converter_tx, from_main_rx) = unbounded(); - let (to_main_tx, from_converter_rx) = unbounded(); - - let mut picker = Picker::new(font_size); - picker.protocol_type = ProtocolType::Kitty; - - tokio::spawn(run_conversion_loop(to_main_tx, from_main_rx, picker)); - - let pages: Vec> = Vec::new(); - let main_area = Rect { x: 0, y: 0, @@ -90,7 +86,37 @@ fn start_all_rendering(path: impl AsRef) -> RenderState { to_render_tx.send(RenderNotif::Area(main_area)).unwrap(); let from_render_rx = from_render_rx.into_stream(); + (from_render_rx, to_render_tx) +} + +pub fn start_converting_loop( + prerender: usize +) -> ( + RecvStream<'static, Result>, + Sender +) { + let (to_converter_tx, from_main_rx) = unbounded(); + let (to_main_tx, from_converter_rx) = unbounded(); + + let mut picker = Picker::new(FONT_SIZE); + picker.protocol_type = ProtocolType::Kitty; + + tokio::spawn(run_conversion_loop( + to_main_tx, + from_main_rx, + picker, + prerender + )); + let from_converter_rx = from_converter_rx.into_stream(); + (from_converter_rx, to_converter_tx) +} + +pub fn start_all_rendering(path: impl AsRef) -> RenderState { + let (from_render_rx, to_render_tx) = start_rendering_loop(path); + let (from_converter_rx, to_converter_tx) = start_converting_loop(20); + + let pages: Vec> = Vec::new(); RenderState { from_render_rx, @@ -126,31 +152,3 @@ pub async fn render_doc(path: impl AsRef) { // thread will see that this is disconnected and think that we're done communicating with them drop(to_render_tx); } - -#[cfg(test)] -pub async fn render_first_page(path: impl AsRef) { - let RenderState { - mut from_render_rx, - mut from_converter_rx, - mut pages, - mut to_converter_tx, - to_render_tx - } = start_all_rendering(path); - - // we only want to render until the first page is ready to be printed - while pages.is_empty() { - tokio::select! { - Some(renderer_msg) = from_render_rx.next() => { - handle_renderer_msg(renderer_msg, &mut pages, &mut to_converter_tx); - }, - Some(converter_msg) = from_converter_rx.next() => { - handle_converter_msg(converter_msg, &mut pages, &mut to_converter_tx); - } - } - } - - black_box(pages); - // we want to make sure this is kept around until the end of this function, or else the other - // thread will see that this is disconnected and think that we're done communicating with them - drop(to_render_tx); -} diff --git a/ratatui-image b/ratatui-image index 88876c7..8d368ca 160000 --- a/ratatui-image +++ b/ratatui-image @@ -1 +1 @@ -Subproject commit 88876c749898cce7801379345eb4e0bc69111b8a +Subproject commit 8d368ca87c9b817811ba47d04d188a334e5da5a6 diff --git a/src/converter.rs b/src/converter.rs index a48deb7..a41c801 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -6,8 +6,6 @@ use ratatui_image::{picker::Picker, protocol::Protocol, Resize}; use crate::renderer::{fill_default, PageInfo, RenderError}; -const MAX_ITER: usize = 20; - pub struct ConvertedPage { pub page: Box, pub num: usize, @@ -23,7 +21,8 @@ pub enum ConverterMsg { pub async fn run_conversion_loop( sender: Sender>, receiver: Receiver, - mut picker: Picker + mut picker: Picker, + prerender: usize ) -> Result<(), SendError>> { let mut images = vec![]; let mut page: usize = 0; @@ -32,16 +31,17 @@ pub async fn run_conversion_loop( images: &mut [Option], picker: &mut Picker, page: usize, - iteration: &mut usize + iteration: &mut usize, + prerender: usize ) -> Result, RenderError> { - if images.is_empty() || *iteration >= MAX_ITER { + if images.is_empty() || *iteration >= prerender { return Ok(None); } // This kinda mimics the way the renderer alternates between going above and below the // current page (within the bounds of how many pages there are) until we've done 20 - let idx_start = page.saturating_sub(MAX_ITER / 2); - let idx_end = idx_start.saturating_add(MAX_ITER).min(images.len()); + let idx_start = page.saturating_sub(prerender / 2); + let idx_end = idx_start.saturating_add(prerender).min(images.len()); // then we go through all the indices available to us and find the first one that has an // image available to steal @@ -111,7 +111,7 @@ pub async fn run_conversion_loop( Err(TryRecvError::Disconnected) => return Ok(()) } - match next_page(&mut images, &mut picker, page, &mut iteration) { + match next_page(&mut images, &mut picker, page, &mut iteration, prerender) { Ok(None) => break, Ok(Some(img)) => sender.send(Ok(img))?, Err(e) => sender.send(Err(e))? diff --git a/src/main.rs b/src/main.rs index 7098cac..409d8c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -141,7 +141,7 @@ async fn main() -> Result<(), Box> { let (to_converter, from_main) = flume::unbounded(); let (to_main, from_converter) = flume::unbounded(); - tokio::spawn(run_conversion_loop(to_main, from_main, picker)); + tokio::spawn(run_conversion_loop(to_main, from_main, picker, 20)); let file_name = path .file_name() diff --git a/src/renderer.rs b/src/renderer.rs index 6d05e28..5684a55 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -27,12 +27,14 @@ pub enum RenderInfo { Page(PageInfo) } +#[derive(Clone)] pub struct PageInfo { pub img_data: ImageData, pub page: usize, pub search_results: usize } +#[derive(Clone)] pub struct ImageData { pub data: Vec, pub area: Rect