mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Merge 3d1a014119 into 09165c15dc
This commit is contained in:
commit
720703f628
4 changed files with 231 additions and 6 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -11009,6 +11009,7 @@ dependencies = [
|
||||||
"assets",
|
"assets",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"collections",
|
"collections",
|
||||||
|
"criterion",
|
||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ util.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assets.workspace = true
|
assets.workspace = true
|
||||||
|
criterion.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
fs = {workspace = true, features = ["test-support"]}
|
fs = {workspace = true, features = ["test-support"]}
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
|
@ -50,3 +51,7 @@ languages = { workspace = true, features = ["load-grammars"] }
|
||||||
node_runtime.workspace = true
|
node_runtime.workspace = true
|
||||||
settings = { workspace = true, features = ["test-support"] }
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
util = { workspace = true, features = ["test-support"] }
|
util = { workspace = true, features = ["test-support"] }
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "append_throughput"
|
||||||
|
harness = false
|
||||||
|
|
|
||||||
96
crates/markdown/benches/append_throughput.rs
Normal file
96
crates/markdown/benches/append_throughput.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
//! `Markdown::append` throughput microbench.
|
||||||
|
//!
|
||||||
|
//! Isolates the string-accumulation cost of `Markdown::append` from the
|
||||||
|
//! rest of the widget (background parse task, GPUI app context, etc.) so
|
||||||
|
//! the algorithmic improvement is reproducible in a single command.
|
||||||
|
//!
|
||||||
|
//! - `pre_fix_concat`: reproduces the historical body of `append`, which
|
||||||
|
//! was `self.source = SharedString::new(self.source.to_string() + text)`
|
||||||
|
//! on every call. O(n) per call -> O(n^2) on the streamed total.
|
||||||
|
//! - `post_fix_buffered`: reproduces the new body, which accumulates into
|
||||||
|
//! a `String` buffer with amortised `push_str` and promotes the buffer
|
||||||
|
//! to a `SharedString` once at the end of the stream (the throttle in
|
||||||
|
//! the real widget collapses many appends into one promotion per parse
|
||||||
|
//! cycle; the bench uses one promotion per stream as the best-case
|
||||||
|
//! bound).
|
||||||
|
//!
|
||||||
|
//! Fixture matches the Paneflow hot path observed via flamegraph on the
|
||||||
|
//! ACP streaming buffer drain loop: 60 KB total across 100 chunks of
|
||||||
|
//! 600 B. The ratio reported by the criterion HTML report is the load-
|
||||||
|
//! bearing artefact for the perf claim.
|
||||||
|
//!
|
||||||
|
//! Run via `cargo bench -p markdown --bench append_throughput`. Output
|
||||||
|
//! lands under `target/criterion/markdown_append/`.
|
||||||
|
|
||||||
|
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||||
|
use gpui::SharedString;
|
||||||
|
|
||||||
|
const CHUNKS: usize = 100;
|
||||||
|
const CHUNK_BYTES: usize = 600;
|
||||||
|
const TOTAL_BYTES: usize = CHUNKS * CHUNK_BYTES;
|
||||||
|
|
||||||
|
fn make_chunks() -> Vec<String> {
|
||||||
|
const FIXTURE_BYTES: &[u8] = b"markdown append streaming fixture with code blocks and prose\n";
|
||||||
|
|
||||||
|
(0..CHUNKS)
|
||||||
|
.map(|chunk_ix| {
|
||||||
|
(0..CHUNK_BYTES)
|
||||||
|
.map(|byte_ix| FIXTURE_BYTES[(chunk_ix + byte_ix) % FIXTURE_BYTES.len()] as char)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pre_fix_concat(chunks: &[String]) -> SharedString {
|
||||||
|
let mut source = SharedString::new("");
|
||||||
|
|
||||||
|
for chunk in chunks {
|
||||||
|
// Exact body of the historical `Markdown::append`.
|
||||||
|
source = SharedString::new(source.to_string() + black_box(chunk.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
source
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_fix_buffered(chunks: &[String]) -> SharedString {
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
for chunk in chunks {
|
||||||
|
// Exact body of the new `Markdown::append` after the buffer is
|
||||||
|
// initialised.
|
||||||
|
buf.push_str(black_box(chunk.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One promotion per stream — the best-case bound the throttle in
|
||||||
|
// `parse()` converges towards when many appends land between parse
|
||||||
|
// cycles.
|
||||||
|
SharedString::from(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_append_throughput(c: &mut Criterion) {
|
||||||
|
let chunks = make_chunks();
|
||||||
|
let mut group = c.benchmark_group("markdown_append");
|
||||||
|
group.throughput(Throughput::Bytes(TOTAL_BYTES as u64));
|
||||||
|
|
||||||
|
group.bench_function(BenchmarkId::new("pre_fix_concat", "60kb_100x600b"), |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let source = pre_fix_concat(black_box(&chunks));
|
||||||
|
black_box(source);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function(
|
||||||
|
BenchmarkId::new("post_fix_buffered", "60kb_100x600b"),
|
||||||
|
|b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let source = post_fix_buffered(black_box(&chunks));
|
||||||
|
black_box(source);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(markdown_append, bench_append_throughput);
|
||||||
|
criterion_main!(markdown_append);
|
||||||
|
|
@ -320,6 +320,21 @@ impl MarkdownStyle {
|
||||||
|
|
||||||
pub struct Markdown {
|
pub struct Markdown {
|
||||||
source: SharedString,
|
source: SharedString,
|
||||||
|
/// Pending append buffer.
|
||||||
|
///
|
||||||
|
/// `Markdown::append` historically rebuilt `self.source` as
|
||||||
|
/// `SharedString::new(self.source.to_string() + text)` on every call, which
|
||||||
|
/// is O(n) per append and therefore O(n^2) on the streamed total. Real
|
||||||
|
/// hot path: ACP agent streams drain into `Markdown::append` at frame rate
|
||||||
|
/// (see `crates/acp_thread/src/acp_thread.rs` streaming buffer loop).
|
||||||
|
///
|
||||||
|
/// When `Some`, this buffer is the authoritative content; `self.source` is
|
||||||
|
/// the last promoted snapshot used by the background parse task. The
|
||||||
|
/// buffer is promoted into `self.source` exactly when a new parse task is
|
||||||
|
/// spawned (see `source_for_parse`), so the amortised cost per `append`
|
||||||
|
/// stays at `String::push_str` (~O(1)). `replace`/`reset` invalidate the
|
||||||
|
/// buffer to keep the contract simple.
|
||||||
|
source_buf: Option<String>,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
pressed_link: Option<RenderedLink>,
|
pressed_link: Option<RenderedLink>,
|
||||||
pressed_footnote_ref: Option<RenderedFootnoteRef>,
|
pressed_footnote_ref: Option<RenderedFootnoteRef>,
|
||||||
|
|
@ -512,6 +527,7 @@ impl Markdown {
|
||||||
};
|
};
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
source,
|
source,
|
||||||
|
source_buf: None,
|
||||||
selection: Selection::default(),
|
selection: Selection::default(),
|
||||||
pressed_link: None,
|
pressed_link: None,
|
||||||
pressed_footnote_ref: None,
|
pressed_footnote_ref: None,
|
||||||
|
|
@ -641,7 +657,11 @@ impl Markdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn source(&self) -> &str {
|
pub fn source(&self) -> &str {
|
||||||
&self.source
|
if let Some(buf) = self.source_buf.as_deref() {
|
||||||
|
buf
|
||||||
|
} else {
|
||||||
|
&self.source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn first_code_block_language(&self) -> Option<Arc<Language>> {
|
pub fn first_code_block_language(&self) -> Option<Arc<Language>> {
|
||||||
|
|
@ -667,11 +687,15 @@ impl Markdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
|
pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
|
||||||
self.source = SharedString::new(self.source.to_string() + text);
|
let buf = self
|
||||||
|
.source_buf
|
||||||
|
.get_or_insert_with(|| self.source.to_string());
|
||||||
|
buf.push_str(text);
|
||||||
self.parse(cx);
|
self.parse(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
|
pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||||
|
self.source_buf = None;
|
||||||
self.source = source.into();
|
self.source = source.into();
|
||||||
self.parse(cx);
|
self.parse(cx);
|
||||||
}
|
}
|
||||||
|
|
@ -711,6 +735,7 @@ impl Markdown {
|
||||||
if source == self.source() {
|
if source == self.source() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.source_buf = None;
|
||||||
self.source = source;
|
self.source = source;
|
||||||
self.selection = Selection::default();
|
self.selection = Selection::default();
|
||||||
self.autoscroll_request = None;
|
self.autoscroll_request = None;
|
||||||
|
|
@ -819,8 +844,28 @@ impl Markdown {
|
||||||
self.context_menu_link.as_ref()
|
self.context_menu_link.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Promote any pending append buffer into `self.source` and return a clone
|
||||||
|
/// of the resulting `SharedString`. Callers that are about to spawn a parse
|
||||||
|
/// task use this so the spawned task sees the latest content and the next
|
||||||
|
/// `append` starts from a fresh, empty buffer.
|
||||||
|
fn source_for_parse(&mut self) -> SharedString {
|
||||||
|
if let Some(buf) = self.source_buf.take() {
|
||||||
|
self.source = SharedString::from(buf);
|
||||||
|
}
|
||||||
|
self.source.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn parse(&mut self, cx: &mut Context<Self>) {
|
fn parse(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.source.is_empty() {
|
let is_empty = match self.source_buf.as_deref() {
|
||||||
|
Some(buf) => buf.is_empty(),
|
||||||
|
None => self.source.is_empty(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_empty {
|
||||||
|
self.source_buf = None;
|
||||||
|
if !self.source.is_empty() {
|
||||||
|
self.source = SharedString::default();
|
||||||
|
}
|
||||||
self.should_reparse = false;
|
self.should_reparse = false;
|
||||||
self.pending_parse.take();
|
self.pending_parse.take();
|
||||||
self.parsed_markdown = ParsedMarkdown {
|
self.parsed_markdown = ParsedMarkdown {
|
||||||
|
|
@ -840,11 +885,11 @@ impl Markdown {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.should_reparse = false;
|
self.should_reparse = false;
|
||||||
self.pending_parse = Some(self.start_background_parse(cx));
|
let source = self.source_for_parse();
|
||||||
|
self.pending_parse = Some(self.start_background_parse(source, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
|
fn start_background_parse(&self, source: SharedString, cx: &Context<Self>) -> Task<()> {
|
||||||
let source = self.source.clone();
|
|
||||||
let should_parse_links_only = self.options.parse_links_only;
|
let should_parse_links_only = self.options.parse_links_only;
|
||||||
let should_parse_html = self.options.parse_html;
|
let should_parse_html = self.options.parse_html;
|
||||||
let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
|
let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
|
||||||
|
|
@ -4673,4 +4718,82 @@ mod tests {
|
||||||
"H3 line height ({h3_line_height:?}) should be greater than body text ({body_line_height:?})"
|
"H3 line height ({h3_line_height:?}) should be greater than body text ({body_line_height:?})"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn append_buffered_accumulates_correctly(cx: &mut TestAppContext) {
|
||||||
|
struct TestWindow;
|
||||||
|
impl Render for TestWindow {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_theme_initialized(cx);
|
||||||
|
let (_, cx) = cx.add_window_view(|_, _| TestWindow);
|
||||||
|
let markdown = cx.new(|cx| Markdown::new("prefix".into(), None, None, cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
markdown.update(cx, |md, cx| {
|
||||||
|
for _ in 0..1000 {
|
||||||
|
md.append("x", cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
let expected = format!("prefix{}", "x".repeat(1000));
|
||||||
|
assert_eq!(markdown.read(cx).source(), expected.as_str());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn source_getter_reflects_pending_buffer(cx: &mut TestAppContext) {
|
||||||
|
struct TestWindow;
|
||||||
|
impl Render for TestWindow {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_theme_initialized(cx);
|
||||||
|
let (_, cx) = cx.add_window_view(|_, _| TestWindow);
|
||||||
|
let markdown = cx.new(|cx| Markdown::new("hello".into(), None, None, cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// First append spawns a background parse, so the second append's parse()
|
||||||
|
// call short-circuits on `pending_parse.is_some()` and leaves the buffer
|
||||||
|
// un-promoted. The getter must still reflect the accumulated content.
|
||||||
|
markdown.update(cx, |md, cx| {
|
||||||
|
md.append(" world", cx);
|
||||||
|
md.append("!", cx);
|
||||||
|
assert!(md.source_buf.is_some());
|
||||||
|
assert_eq!(md.source(), "hello world!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn replace_clears_pending_buffer(cx: &mut TestAppContext) {
|
||||||
|
struct TestWindow;
|
||||||
|
impl Render for TestWindow {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_theme_initialized(cx);
|
||||||
|
let (_, cx) = cx.add_window_view(|_, _| TestWindow);
|
||||||
|
let markdown = cx.new(|cx| Markdown::new("".into(), None, None, cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
markdown.update(cx, |md, cx| {
|
||||||
|
// Two appends ensures the second one leaves a populated `source_buf`
|
||||||
|
// (the first append's spawned parse is still pending).
|
||||||
|
md.append("a", cx);
|
||||||
|
md.append("a", cx);
|
||||||
|
assert!(md.source_buf.is_some());
|
||||||
|
md.replace(SharedString::new_static("b"), cx);
|
||||||
|
assert!(md.source_buf.is_none());
|
||||||
|
assert_eq!(md.source(), "b");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue