Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 71 additions & 34 deletions crates/editor/src/content/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use itertools::Itertools;
use markdown_parser::{Hyperlink, TableAlignment};
use num_traits::SaturatingSub;
use rangemap::RangeSet;
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use string_offset::{ByteOffset, CharOffset};
use urlocator::{UrlLocation, UrlLocator};
use vec1::Vec1;
Expand All @@ -29,7 +29,6 @@ use super::mermaid_diagram::{mermaid_asset_source, mermaid_diagram_layout};
use super::text::{
BufferBlockItem, BufferBlockStyle, CodeBlockType, FormattedTable, TableBlockCache,
};
use crate::parallel_util::Last;
use crate::render::layout::{InlineTextLayoutInput, TextLayout, add_link_to_style_and_font};
use crate::render::model::{
BlockItem, BlockLocation, BlockSpacing, CellLayout, Cursor, Decoration, FrameOffset,
Expand Down Expand Up @@ -491,6 +490,12 @@ impl LayOutArgs {
}
}

// Maximum number of layout tasks to execute in parallel at once. Running all tasks in
// parallel on large documents causes memory spikes from simultaneous CoreText layout
// allocations across all rayon worker threads. Sequential chunking bounds peak memory
// to CHUNK_SIZE tasks at a time while preserving parallelism within each chunk.
const MAX_PARALLEL_LAYOUT_TASKS: usize = 8;

impl EditDelta {
/// Lay out the given EditDelta into TextFrames.
/// If hidden_lines is provided, lines within hidden ranges will be laid out as BlockItem::Hidden.
Expand Down Expand Up @@ -533,34 +538,57 @@ impl EditDelta {
.collect();

let last_task = layout_tasks.len().saturating_sub(1);
let old_offset = self.old_offset.clone();

// Then, run each task in parallel, collecting (a) the laid out BlockItems and (b) whether
// or not the last item ends with a newline.
let (block_items, has_trailing_newline): (Vec<_>, Last<_>) = layout_tasks
.into_par_iter()
// Pre-annotate each task with its global BlockLocation so it can be computed
// before we split into chunks (the global index is lost inside each chunk).
let annotated_tasks: Vec<(LayoutTask, bool, BlockLocation)> = layout_tasks
.into_iter()
.enumerate()
.filter_map(|(idx, (task, is_hidden))| {
.map(|(idx, (task, is_hidden))| {
let location = if idx == 0 {
BlockLocation::Start
} else if idx >= last_task {
BlockLocation::End
} else {
BlockLocation::Middle
};
(task, is_hidden, location)
})
.collect();

match task.run(layout, location, is_hidden) {
Ok(result) => Some(result),
Err(e) => {
log::error!(
"Failed to lay out BlockItem at offset {:?}: {:?}",
self.old_offset,
e
);
None
// Run tasks in sequential parallel chunks to bound peak memory. See APP-4686.
// Without chunking, all tasks run simultaneously, causing 9+ GB memory spikes
// on large documents from concurrent CoreText layout allocations.
let mut all_results: Vec<(BlockItem, bool)> = Vec::with_capacity(annotated_tasks.len());
for chunk in &annotated_tasks
.into_iter()
.chunks(MAX_PARALLEL_LAYOUT_TASKS)
{
let chunk: Vec<_> = chunk.collect();
let chunk_results: Vec<_> = chunk
.into_par_iter()
.filter_map(|(task, is_hidden, location)| {
match task.run(layout, location, is_hidden) {
Ok(result) => Some(result),
Err(e) => {
log::error!(
"Failed to lay out BlockItem at offset {:?}: {:?}",
old_offset,
e
);
None
}
}
}
})
.unzip();
})
.collect();
all_results.extend(chunk_results);
}
let has_trailing_newline = all_results
.last()
.map(|(_, has_newline)| *has_newline)
.unwrap_or(true);
let block_items: Vec<_> = all_results.into_iter().map(|(item, _)| item).collect();

// Iterate through block_items, and collapse adjacent Hidden items.
let block_items = block_items.into_iter().fold(Vec::new(), |mut acc, item| {
Expand All @@ -578,10 +606,6 @@ impl EditDelta {
acc
});

// Trailing newline is default to true. This default value is used when
// edit delta has no new line, which means one or multiple entire lines have
// been deleted. We should still leave a trailing newline in this case.
let has_trailing_newline = has_trailing_newline.into_inner().unwrap_or(true);
let rich_text_styles = layout.rich_text_styles();

LaidOutRenderDelta {
Expand Down Expand Up @@ -622,28 +646,41 @@ pub fn layout_temporary_blocks(

let last_task = layout_tasks.len().saturating_sub(1);

let results: Vec<_> = layout_tasks
.into_par_iter()
// Pre-annotate with location and process in sequential parallel chunks
// to bound peak memory (same rationale as EditDelta::layout_delta).
let annotated: Vec<(LayoutTask, LineCount, BlockLocation)> = layout_tasks
.into_iter()
.enumerate()
.filter_map(|(idx, (task, line_count))| {
.map(|(idx, (task, line_count))| {
let location = if idx == 0 {
BlockLocation::Start
} else if idx >= last_task {
BlockLocation::End
} else {
BlockLocation::Middle
};

match task.run(layout, location, false) {
Ok(result) => Some((line_count, result.0)),
Err(e) => {
log::error!("Failed to lay out temporary blocks: {e:?}");
None
}
}
(task, line_count, location)
})
.collect();

let mut results: Vec<(LineCount, BlockItem)> = Vec::with_capacity(annotated.len());
for chunk in &annotated.into_iter().chunks(MAX_PARALLEL_LAYOUT_TASKS) {
let chunk: Vec<_> = chunk.collect();
let chunk_results: Vec<_> = chunk
.into_par_iter()
.filter_map(
|(task, line_count, location)| match task.run(layout, location, false) {
Ok(result) => Some((line_count, result.0)),
Err(e) => {
log::error!("Failed to lay out temporary blocks: {e:?}");
None
}
},
)
.collect();
results.extend(chunk_results);
}

results.into_iter().into_group_map()
}

Expand Down
1 change: 0 additions & 1 deletion crates/editor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pub mod decoration;
pub mod editor;
pub mod model;
pub mod multiline;
mod parallel_util;
pub mod render;
pub mod search;
pub mod selection;
46 changes: 0 additions & 46 deletions crates/editor/src/parallel_util.rs

This file was deleted.