From e2db3b0c65c19a7d374171239e12058ad0ab2f2e Mon Sep 17 00:00:00 2001 From: Oz Date: Mon, 1 Jun 2026 18:58:09 +0000 Subject: [PATCH] fix: add 50MB file size limit to prevent memory spike in editor layout When a very large file is opened in the code editor, the buffer loading and layout systems can consume excessive memory. The editor's layout system (SumTree) creates multiple in-memory structures per line, so a file that is large on disk can consume 5-10x more memory once fully laid out. This commit adds a 50MB file size limit check in FileModel::open and FileModel::read_content_for_file. Files exceeding this threshold are rejected with FileLoadError::FileTooLarge instead of being loaded into memory and laid out. Sentry issue: https://sentry.io/organizations/warpdotdev/issues/7259255054/ Heap profile evidence: SumTree::push consumed 12.67GB (72.59% of 17.45GB total) during RenderState::layout_edit_delta. Co-Authored-By: Oz --- app/src/ai/blocklist/action_model/execute.rs | 1 + crates/warp_files/src/lib.rs | 39 ++++++++++++++++++-- crates/warp_util/src/file.rs | 2 + 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/src/ai/blocklist/action_model/execute.rs b/app/src/ai/blocklist/action_model/execute.rs index 90df1f83b1..3a0433264a 100644 --- a/app/src/ai/blocklist/action_model/execute.rs +++ b/app/src/ai/blocklist/action_model/execute.rs @@ -1175,6 +1175,7 @@ async fn read_binary_file_context( Ok(content) => content, Err(FileLoadError::DoesNotExist) => return Ok(BinaryFileReadResult::Missing), Err(FileLoadError::IOError(e)) => return Err(anyhow::anyhow!(e)), + Err(FileLoadError::FileTooLarge { .. }) => return Ok(BinaryFileReadResult::Missing), }; let mime_type = from_path(path).first_or_octet_stream().to_string(); diff --git a/crates/warp_files/src/lib.rs b/crates/warp_files/src/lib.rs index 551c73684e..dfdcd9c4db 100644 --- a/crates/warp_files/src/lib.rs +++ b/crates/warp_files/src/lib.rs @@ -31,6 +31,14 @@ use watcher::{BulkFilesystemWatcher, BulkFilesystemWatcherEvent}; pub mod text_file_reader; pub use text_file_reader::{TextFileReadResult, TextFileSegment}; +/// Maximum file size (in bytes) that the code editor will load into memory. +/// Files larger than this threshold are rejected with `FileLoadError::FileTooLarge` +/// to prevent excessive memory usage during buffer loading and layout. +/// The editor's layout system creates multiple in-memory structures per line +/// (BlockItem nodes, text frames, selections, etc.), so a file that is large on +/// disk can consume 5-10x more memory once fully laid out. +const MAX_EDITOR_FILE_SIZE: u64 = 50 * 1024 * 1024; // 50 MB + #[derive(Debug)] pub enum FileModelEvent { FileLoaded { @@ -411,9 +419,7 @@ impl FileModel { let use_individual_watcher = watcher_type == WatcherType::Individual; let future = ctx.spawn( async move { - let contents = async_fs::read_to_string(&file_path_buf) - .await - .map_err(FileLoadError::from); + let contents = Self::read_content_with_size_check(&file_path_buf).await; (file_id, contents) }, move |me, (file_id, load_result), ctx| match load_result { @@ -467,11 +473,38 @@ impl FileModel { if !Self::file_exists(file_path).await { return Err(FileLoadError::DoesNotExist); } + Self::check_file_size(file_path).await?; + async_fs::read_to_string(file_path) + .await + .map_err(FileLoadError::from) + } + + /// Read a file's content after checking that it does not exceed + /// [`MAX_EDITOR_FILE_SIZE`]. Used by [`Self::open`] to prevent loading + /// files that are too large for the editor's layout system. + async fn read_content_with_size_check(file_path: &Path) -> Result { + Self::check_file_size(file_path).await?; async_fs::read_to_string(file_path) .await .map_err(FileLoadError::from) } + /// Returns `Err(FileLoadError::FileTooLarge)` if `file_path` exceeds + /// [`MAX_EDITOR_FILE_SIZE`]. + async fn check_file_size(file_path: &Path) -> Result<(), FileLoadError> { + let metadata = async_fs::metadata(file_path) + .await + .map_err(FileLoadError::from)?; + let size = metadata.len(); + if size > MAX_EDITOR_FILE_SIZE { + return Err(FileLoadError::FileTooLarge { + size_bytes: size, + limit_bytes: MAX_EDITOR_FILE_SIZE, + }); + } + Ok(()) + } + /// Asynchronously reads specific lines from a file using BufReader. /// /// # Arguments diff --git a/crates/warp_util/src/file.rs b/crates/warp_util/src/file.rs index 85c976ff43..3bec5b4d42 100644 --- a/crates/warp_util/src/file.rs +++ b/crates/warp_util/src/file.rs @@ -22,6 +22,8 @@ pub enum FileLoadError { DoesNotExist, #[error("IO error when loading file.")] IOError(#[from] io::Error), + #[error("File is too large to open in the editor ({size_bytes} bytes, limit is {limit_bytes} bytes)")] + FileTooLarge { size_bytes: u64, limit_bytes: u64 }, } #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]