diff --git a/Cargo.lock b/Cargo.lock index 127c329710d..5dc2fed54f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2151,6 +2151,7 @@ dependencies = [ "language", "language_model", "lsp", + "markdown", "multi_buffer", "project", "prompt_store", diff --git a/crates/benchmarks/Cargo.toml b/crates/benchmarks/Cargo.toml index 5a62d0273d9..1db9acf391b 100644 --- a/crates/benchmarks/Cargo.toml +++ b/crates/benchmarks/Cargo.toml @@ -12,6 +12,9 @@ doctest = false [lints] workspace = true +[dependencies] +rand.workspace = true + [dev-dependencies] action_log.workspace = true agent = { workspace = true, features = ["test-support"] } @@ -26,10 +29,10 @@ itertools.workspace = true language = { workspace = true, features = ["test-support"] } language_model = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } +markdown.workspace = true multi_buffer.workspace = true project = { workspace = true, features = ["test-support"] } prompt_store.workspace = true -rand.workspace = true serde_json.workspace = true settings = { workspace = true, features = ["test-support"] } text.workspace = true @@ -50,3 +53,7 @@ harness = false [[bench]] name = "edit_file_tool" harness = false + +[[bench]] +name = "markdown_renderer" +harness = false diff --git a/crates/benchmarks/benches/edit_file_tool.rs b/crates/benchmarks/benches/edit_file_tool.rs index 7080b01200e..a00add54ee3 100644 --- a/crates/benchmarks/benches/edit_file_tool.rs +++ b/crates/benchmarks/benches/edit_file_tool.rs @@ -12,6 +12,10 @@ use agent::{ Templates, Thread, ToolCallEventStream, ToolInput, }; use agent_settings::{AgentSettings, ToolRules}; +use benchmarks::bench_utils::{ + RUST_FUNCTION_BODY_LINES, RUST_FUNCTION_LINES, RUST_MODULE_HEADER_LINES, random_rust_file, + rust_file_line_count, rust_identifier as identifier, +}; use criterion::{ BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main, }; @@ -478,7 +482,7 @@ fn make_fixture( seed: u64, ) -> EditFixture { let mut rng = StdRng::seed_from_u64(seed); - let old_lines = random_rust_module(&mut rng, function_count); + let old_lines = random_rust_file(&mut rng, rust_file_line_count(function_count)); let edit_range = edit_range(&old_lines, &pattern); let old_text = old_lines[edit_range.clone()].join("\n"); let mut new_lines = old_lines.clone(); @@ -514,12 +518,8 @@ fn make_large_multi_edit_fixture( edit_count: usize, seed: u64, ) -> EditFixture { - const HEADER_LINES: usize = 10; - const FUNCTION_LINES: usize = 12; - const FUNCTION_BODY_LINES: usize = 11; - let mut rng = StdRng::seed_from_u64(seed); - let old_lines = random_rust_module(&mut rng, function_count); + let old_lines = random_rust_file(&mut rng, rust_file_line_count(function_count)); let old_file_text = old_lines.join("\n"); let step = (function_count / edit_count).max(1); @@ -541,8 +541,8 @@ fn make_large_multi_edit_fixture( let edits = replacements .iter() .map(|(function_index, new_function)| { - let start = HEADER_LINES + function_index * FUNCTION_LINES; - let end = start + FUNCTION_BODY_LINES; + let start = RUST_MODULE_HEADER_LINES + function_index * RUST_FUNCTION_LINES; + let end = start + RUST_FUNCTION_BODY_LINES; EditOp { old_text: old_lines[start..end].join("\n"), new_text: new_function.join("\n"), @@ -552,8 +552,8 @@ fn make_large_multi_edit_fixture( let mut new_lines = old_lines; for (function_index, new_function) in replacements.iter().rev() { - let start = HEADER_LINES + function_index * FUNCTION_LINES; - let end = start + FUNCTION_BODY_LINES; + let start = RUST_MODULE_HEADER_LINES + function_index * RUST_FUNCTION_LINES; + let end = start + RUST_FUNCTION_BODY_LINES; new_lines.splice(start..end, new_function.iter().cloned()); } let expected_file_text = new_lines.join("\n"); @@ -622,56 +622,6 @@ fn edit_range(lines: &[String], pattern: &EditPattern) -> std::ops::Range range } -fn random_rust_module(rng: &mut StdRng, function_count: usize) -> Vec { - let mut lines = vec![ - "use anyhow::{Context as _, Result};".to_string(), - "use collections::HashMap;".to_string(), - "".to_string(), - "#[derive(Clone, Debug)]".to_string(), - "pub struct WorkspaceSnapshot {".to_string(), - " buffers: HashMap,".to_string(), - " version: usize,".to_string(), - "}".to_string(), - "".to_string(), - "impl WorkspaceSnapshot {".to_string(), - ]; - - for function_index in 0..function_count { - let function_name = identifier(rng, function_index); - let argument_name = identifier(rng, function_index + 1_000); - let local_name = identifier(rng, function_index + 2_000); - let branch_name = identifier(rng, function_index + 3_000); - let multiplier = rng.random_range(2..17); - let offset = rng.random_range(1..128); - - lines.extend([ - format!( - " pub fn {function_name}(&mut self, {argument_name}: usize) -> Result {{" - ), - format!(" let mut {local_name} = {argument_name}.saturating_mul({multiplier});"), - format!(" if {local_name} % 2 == 0 {{"), - format!( - " {local_name} = {local_name}.saturating_add(self.version + {offset});" - ), - " } else {".to_string(), - format!(" {local_name} = {local_name}.saturating_sub({offset});"), - " }".to_string(), - format!(" let {branch_name} = self.buffers.len().saturating_add({local_name});"), - format!(" self.version = self.version.saturating_add({branch_name});"), - format!(" Ok({branch_name})"), - " }".to_string(), - "".to_string(), - ]); - } - - lines.push("}".to_string()); - lines.push("".to_string()); - lines.push("pub fn normalize_path(path: &str) -> String {".to_string()); - lines.push(" path.replace('\\\\', \"/\")".to_string()); - lines.push("}".to_string()); - lines -} - fn rewrite_local_block(lines: &mut [String], rng: &mut StdRng) { for (line_index, line) in lines.iter_mut().enumerate() { let suffix = identifier(rng, line_index + 10_000); @@ -727,17 +677,5 @@ fn insert_helper_blocks( } } -fn identifier(rng: &mut StdRng, salt: usize) -> String { - const PARTS: &[&str] = &[ - "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "theta", "lambda", "sigma", "omega", - ]; - format!( - "{}_{}_{}", - PARTS[rng.random_range(0..PARTS.len())], - salt, - rng.random_range(0..10_000) - ) -} - criterion_group!(benches, edit_file_tool_streaming); criterion_main!(benches); diff --git a/crates/benchmarks/benches/markdown_renderer.rs b/crates/benchmarks/benches/markdown_renderer.rs new file mode 100644 index 00000000000..7320bbd8099 --- /dev/null +++ b/crates/benchmarks/benches/markdown_renderer.rs @@ -0,0 +1,348 @@ +use benchmarks::bench_utils::random_rust_file; +use gpui::{ + AppContext as _, BenchAppContext, Context, Entity, IntoElement, Render, SharedString, Window, +}; +use language::LanguageRegistry; +use markdown::{ + CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, + MarkdownOptions, MarkdownStyle, WrapButtonVisibility, +}; +use rand::{Rng as _, SeedableRng as _, rngs::StdRng}; +use settings::SettingsStore; +use std::sync::Arc; +use ui::prelude::*; + +const SEED: u64 = 1; + +#[gpui::bench( + inputs = markdown_sizes(), + group = "Markdown render", + input_name = "min_bytes", + sample_size = 10 +)] +fn markdown_render(target_size: &usize, cx: &mut BenchAppContext) { + init_context(cx); + + let source = SharedString::from(generate_markdown(SEED, *target_size)); + let language_registry = markdown_language_registry(cx); + + let mut window = cx.add_empty_window(); + let view = window.update(|window, cx| { + let markdown = cx.new({ + let source = source.clone(); + let language_registry = language_registry.clone(); + move |cx| build_markdown(source, language_registry, cx) + }); + window.replace_root(cx, |_window, _cx| MarkdownBenchView { markdown }) + }); + + cx.bench_renderer(view, |_, _, cx| cx.notify()); +} + +struct MarkdownBenchView { + markdown: Entity, +} + +impl Render for MarkdownBenchView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let style = MarkdownStyle::themed(MarkdownFont::Preview, window, cx); + + div().w_full().h_full().child( + MarkdownElement::new(self.markdown.clone(), style).code_block_renderer( + CodeBlockRenderer::Default { + copy_button_visibility: CopyButtonVisibility::VisibleOnHover, + wrap_button_visibility: WrapButtonVisibility::VisibleOnHover, + border: false, + }, + ), + ) + } +} + +fn build_markdown( + source: SharedString, + language_registry: Arc, + cx: &mut Context, +) -> Markdown { + Markdown::new_with_options( + source, + Some(language_registry), + None, + MarkdownOptions { + // Mermaid and embedded resources need their own focused benchmarks. + render_metadata_blocks: true, + ..Default::default() + }, + cx, + ) +} + +fn generate_markdown(seed: u64, target_size: usize) -> String { + let mut rng = StdRng::seed_from_u64(seed); + let mut markdown = String::with_capacity(target_size); + + markdown.push_str("---\n"); + markdown.push_str("title: Markdown renderer benchmark\n"); + markdown.push_str("author: Zed benchmark\n"); + markdown.push_str("---\n\n"); + + while markdown.len() < target_size { + push_mixed_block(&mut markdown, &mut rng); + } + + markdown +} + +fn push_mixed_block(markdown: &mut String, rng: &mut StdRng) { + match rng.random_range(0..10) { + 0 => push_heading(markdown, rng), + 1 | 2 => push_paragraph(markdown, rng), + 3 => push_list(markdown, rng), + 4 => push_task_list(markdown, rng), + 5 => push_table(markdown, rng), + 6 | 7 => push_code_block(markdown, rng), + 8 => push_block_quote(markdown, rng), + _ => push_rule(markdown), + } +} + +fn push_heading(markdown: &mut String, rng: &mut StdRng) { + let level = rng.random_range(1..=4); + markdown.push_str(&"#".repeat(level)); + markdown.push(' '); + let word_count = rng.random_range(3..8); + push_words(markdown, rng, word_count); + markdown.push_str("\n\n"); +} + +fn push_paragraph(markdown: &mut String, rng: &mut StdRng) { + let sentence_count = rng.random_range(2..7); + for sentence_index in 0..sentence_count { + if sentence_index > 0 { + markdown.push(' '); + } + push_sentence(markdown, rng); + } + markdown.push_str("\n\n"); +} + +fn push_sentence(markdown: &mut String, rng: &mut StdRng) { + let word_count = rng.random_range(8..24); + for word_index in 0..word_count { + if word_index > 0 { + markdown.push(' '); + } + + match rng.random_range(0..18) { + 0 => { + markdown.push_str("**"); + markdown.push_str(random_word(rng)); + markdown.push_str("**"); + } + 1 => { + markdown.push('*'); + markdown.push_str(random_word(rng)); + markdown.push('*'); + } + 2 => { + markdown.push('`'); + markdown.push_str(random_identifier(rng)); + markdown.push('`'); + } + 3 => { + markdown.push('['); + markdown.push_str(random_word(rng)); + markdown.push_str("](https://example.com/"); + markdown.push_str(random_identifier(rng)); + markdown.push(')'); + } + 4 => { + markdown.push_str("https://zed.dev/"); + markdown.push_str(random_identifier(rng)); + } + _ => markdown.push_str(random_word(rng)), + } + } + markdown.push('.'); +} + +fn push_list(markdown: &mut String, rng: &mut StdRng) { + let item_count = rng.random_range(3..9); + let ordered = rng.random(); + for item_index in 0..item_count { + if ordered { + markdown.push_str(&(item_index + 1).to_string()); + markdown.push_str(". "); + } else { + markdown.push_str("- "); + } + let word_count = rng.random_range(5..14); + push_words(markdown, rng, word_count); + markdown.push('\n'); + } + markdown.push('\n'); +} + +fn push_task_list(markdown: &mut String, rng: &mut StdRng) { + let item_count = rng.random_range(3..8); + for _ in 0..item_count { + if rng.random() { + markdown.push_str("- [x] "); + } else { + markdown.push_str("- [ ] "); + } + let word_count = rng.random_range(4..12); + push_words(markdown, rng, word_count); + markdown.push('\n'); + } + markdown.push('\n'); +} + +fn push_table(markdown: &mut String, rng: &mut StdRng) { + let column_count = rng.random_range(3..7); + let row_count = rng.random_range(3..9); + + for column_index in 0..column_count { + if column_index == 0 { + markdown.push('|'); + } + markdown.push(' '); + markdown.push_str(random_word(rng)); + markdown.push(' '); + markdown.push('|'); + } + markdown.push('\n'); + + for column_index in 0..column_count { + if column_index == 0 { + markdown.push('|'); + } + markdown.push_str(" --- |"); + } + markdown.push('\n'); + + for _ in 0..row_count { + for column_index in 0..column_count { + if column_index == 0 { + markdown.push('|'); + } + markdown.push(' '); + let word_count = rng.random_range(2..6); + push_words(markdown, rng, word_count); + markdown.push(' '); + markdown.push('|'); + } + markdown.push('\n'); + } + markdown.push('\n'); +} + +fn push_code_block(markdown: &mut String, rng: &mut StdRng) { + let line_count = rng.random_range(24..80); + let rust = random_rust_file(rng, line_count); + markdown.push_str("```rust\n"); + markdown.push_str(&rust.join("\n")); + markdown.push_str("\n```\n\n"); +} + +fn push_block_quote(markdown: &mut String, rng: &mut StdRng) { + let line_count = rng.random_range(2..6); + for _ in 0..line_count { + markdown.push_str("> "); + push_sentence(markdown, rng); + markdown.push('\n'); + } + markdown.push('\n'); +} + +fn push_rule(markdown: &mut String) { + markdown.push_str("---\n\n"); +} + +fn push_words(markdown: &mut String, rng: &mut StdRng, word_count: usize) { + for word_index in 0..word_count { + if word_index > 0 { + markdown.push(' '); + } + markdown.push_str(random_word(rng)); + } +} + +fn random_word(rng: &mut StdRng) -> &'static str { + const WORDS: &[&str] = &[ + "renderer", + "layout", + "markdown", + "paragraph", + "heading", + "table", + "selection", + "syntax", + "highlight", + "window", + "element", + "callback", + "benchmark", + "profile", + "latency", + "throughput", + "scroll", + "wrapping", + "theme", + "language", + "fenced", + "blockquote", + "inline", + "content", + ]; + choose(rng, WORDS) +} + +fn random_identifier(rng: &mut StdRng) -> &'static str { + const IDENTIFIERS: &[&str] = &[ + "markdown_renderer", + "layout_cache", + "rendered_text", + "source_range", + "bench_input", + "window_state", + "code_block", + "table_row", + "link_target", + "scroll_handle", + "text_style", + "root_block", + ]; + choose(rng, IDENTIFIERS) +} + +fn choose(rng: &mut StdRng, items: &'static [&'static str]) -> &'static str { + let index = rng.random_range(0..items.len()); + items.get(index).copied().unwrap_or("markdown") +} + +fn markdown_language_registry(cx: &BenchAppContext) -> Arc { + let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); + registry.add(language::rust_lang()); + registry +} + +fn init_context(cx: &mut BenchAppContext) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + assets::Assets.load_test_fonts(cx); + theme_settings::init(theme::LoadThemes::JustBase, cx); + }); +} + +fn markdown_sizes() -> Vec { + let mut sizes = vec![5_000, 10_000, 50_000, 250_000]; + if std::env::var("ZED_BENCH_HUGE").is_ok() { + sizes.push(1_000_000); + } + sizes +} + +gpui::bench_group!(benches, markdown_render); +gpui::bench_main!(benches); diff --git a/crates/benchmarks/src/bench_utils.rs b/crates/benchmarks/src/bench_utils.rs new file mode 100644 index 00000000000..d35d76d708c --- /dev/null +++ b/crates/benchmarks/src/bench_utils.rs @@ -0,0 +1,89 @@ +use rand::Rng; + +pub const RUST_MODULE_HEADER_LINES: usize = 10; +pub const RUST_FUNCTION_LINES: usize = 12; +pub const RUST_FUNCTION_BODY_LINES: usize = 11; +pub const RUST_MODULE_FOOTER_LINES: usize = 5; + +pub fn rust_file_line_count(function_count: usize) -> usize { + RUST_MODULE_HEADER_LINES + function_count * RUST_FUNCTION_LINES + RUST_MODULE_FOOTER_LINES +} + +pub fn random_rust_file(rng: &mut impl Rng, line_count: usize) -> Vec { + if line_count < RUST_MODULE_HEADER_LINES + RUST_MODULE_FOOTER_LINES { + return (0..line_count) + .map(|line_index| format!("// generated benchmark line {line_index}")) + .collect(); + } + + let mut lines = vec![ + "use anyhow::{Context as _, Result};".to_string(), + "use collections::HashMap;".to_string(), + "".to_string(), + "#[derive(Clone, Debug)]".to_string(), + "pub struct WorkspaceSnapshot {".to_string(), + " buffers: HashMap,".to_string(), + " version: usize,".to_string(), + "}".to_string(), + "".to_string(), + "impl WorkspaceSnapshot {".to_string(), + ]; + + let body_line_count = line_count - RUST_MODULE_HEADER_LINES - RUST_MODULE_FOOTER_LINES; + let function_count = body_line_count / RUST_FUNCTION_LINES; + let filler_line_count = body_line_count % RUST_FUNCTION_LINES; + + for function_index in 0..function_count { + let function_name = rust_identifier(rng, function_index); + let argument_name = rust_identifier(rng, function_index + 1_000); + let local_name = rust_identifier(rng, function_index + 2_000); + let branch_name = rust_identifier(rng, function_index + 3_000); + let multiplier = rng.random_range(2..17); + let offset = rng.random_range(1..128); + + lines.extend([ + format!( + " pub fn {function_name}(&mut self, {argument_name}: usize) -> Result {{" + ), + format!(" let mut {local_name} = {argument_name}.saturating_mul({multiplier});"), + format!(" if {local_name} % 2 == 0 {{"), + format!( + " {local_name} = {local_name}.saturating_add(self.version + {offset});" + ), + " } else {".to_string(), + format!(" {local_name} = {local_name}.saturating_sub({offset});"), + " }".to_string(), + format!(" let {branch_name} = self.buffers.len().saturating_add({local_name});"), + format!(" self.version = self.version.saturating_add({branch_name});"), + format!(" Ok({branch_name})"), + " }".to_string(), + "".to_string(), + ]); + } + + for filler_index in 0..filler_line_count { + let filler_name = rust_identifier(rng, function_count + 4_000 + filler_index); + lines.push(format!(" // benchmark filler {filler_name}")); + } + + lines.push("}".to_string()); + lines.push("".to_string()); + lines.push("pub fn normalize_path(path: &str) -> String {".to_string()); + lines.push(" path.replace('\\\\', \"/\")".to_string()); + lines.push("}".to_string()); + + debug_assert_eq!(lines.len(), line_count); + lines +} + +pub fn rust_identifier(rng: &mut impl Rng, salt: usize) -> String { + const PARTS: &[&str] = &[ + "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "theta", "lambda", "sigma", "omega", + ]; + format!( + "{}_{}_{}", + PARTS[rng.random_range(0..PARTS.len())], + salt, + rng.random_range(0..10_000) + ) +} diff --git a/crates/benchmarks/src/benchmarks.rs b/crates/benchmarks/src/benchmarks.rs index 1b606305052..4c4134b5b94 100644 --- a/crates/benchmarks/src/benchmarks.rs +++ b/crates/benchmarks/src/benchmarks.rs @@ -4,3 +4,5 @@ //! (Criterion, `gpui_platform`, gpui's `bench` feature, ...) don't weigh down //! the test builds of the crates being benchmarked. Each file in `benches/` //! targets one area of the codebase. + +pub mod bench_utils; diff --git a/crates/gpui/src/app/bench_context.rs b/crates/gpui/src/app/bench_context.rs index 35d4144566f..3085776d702 100644 --- a/crates/gpui/src/app/bench_context.rs +++ b/crates/gpui/src/app/bench_context.rs @@ -409,9 +409,14 @@ impl<'a, 'measurement> BenchAppContext<'a, 'measurement> { }) .expect("cannot benchmark renderer for entity without a current window"); + let dispatcher = self.background_executor.dispatcher().clone(); let collector = FrameTraceScope::start(); let mut benchmark = || { + dispatcher + .as_bench() + .expect("validated in BenchAppContext::build") + .run_ready_main_tasks(); self.with_window(view.entity_id(), |window, cx| { view.update(cx, |view, cx| update(view, window, cx)); }) diff --git a/crates/gpui/src/platform/bench_dispatcher.rs b/crates/gpui/src/platform/bench_dispatcher.rs index 91e4a27c82f..139434bd187 100644 --- a/crates/gpui/src/platform/bench_dispatcher.rs +++ b/crates/gpui/src/platform/bench_dispatcher.rs @@ -258,6 +258,16 @@ impl BenchDispatcher { } } + /// Runs all main-thread tasks that are queued right now, without waiting for + /// background work or timers to finish. + pub fn run_ready_main_tasks(&self) -> bool { + assert!( + self.is_main_thread(), + "run_ready_main_tasks must be called on the benchmark main thread" + ); + self.drain_main_queue() + } + /// Cancels all pending timers so timers armed by one benchmark can't fire /// during a later benchmark sharing this process-lifetime dispatcher. /// @@ -379,6 +389,38 @@ mod tests { use super::*; use crate::{BackgroundExecutor, ForegroundExecutor}; + #[test] + fn run_ready_main_tasks_does_not_wait_for_background_handoffs() { + let dispatcher = Arc::new(BenchDispatcher::new()); + let background = BackgroundExecutor::new(dispatcher.clone()); + let foreground = ForegroundExecutor::new(dispatcher.clone()); + + let (sender, receiver) = futures::channel::oneshot::channel(); + background + .spawn(async move { + thread::sleep(Duration::from_millis(10)); + sender.send(()).ok(); + }) + .detach(); + + let completed = Arc::new(AtomicBool::new(false)); + foreground + .spawn({ + let completed = completed.clone(); + async move { + receiver.await.ok(); + completed.store(true, Ordering::SeqCst); + } + }) + .detach(); + + assert!(dispatcher.run_ready_main_tasks()); + assert!(!completed.load(Ordering::SeqCst)); + + dispatcher.run_until_idle(); + assert!(completed.load(Ordering::SeqCst)); + } + #[test] fn run_until_idle_completes_background_to_main_handoffs() { let dispatcher = Arc::new(BenchDispatcher::new());