Compare commits

...

4 Commits

Author SHA1 Message Date
Jakub Konka
83357cd96a buffer: Bring back saving reparse task handle 2025-11-21 17:57:00 +01:00
Jakub Konka
be9bb2f11a Buffer: Remove blocking on bg thread in reparsing 2025-11-21 14:47:39 +01:00
Jakub Konka
285965ffa8 Mark hotspots + add some logs 2025-11-20 18:21:54 +01:00
David
408887eeed Rebase on latest main 2025-11-20 18:20:45 +01:00
12 changed files with 194 additions and 80 deletions

View File

@@ -1,6 +1,6 @@
[build]
# v0 mangling scheme provides more detailed backtraces around closures
rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
rustflags = ["-C", "symbol-mangling-version=v0", "-C", "force-frame-pointers=yes", "--cfg", "tokio_unstable"]
[alias]
xtask = "run --package xtask --"
@@ -8,6 +8,14 @@ perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests",
# Keep similar flags here to share some ccache
perf-compare = ["run", "--profile", "release-fast", "-p", "perf", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]", "--", "compare"]
# [target.x86_64-unknown-linux-gnu]
# linker = "clang"
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.'cfg(target_os = "windows")']
rustflags = [
"--cfg",
@@ -16,5 +24,8 @@ rustflags = [
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
]
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "split-debuginfo=packed"]
[env]
MACOSX_DEPLOYMENT_TARGET = "10.15.7"

1
Cargo.lock generated
View File

@@ -10029,6 +10029,7 @@ name = "miniprofiler_ui"
version = "0.1.0"
dependencies = [
"gpui",
"log",
"serde_json",
"smol",
"util",

View File

@@ -859,7 +859,7 @@ ui_input = { codegen-units = 1 }
zed_actions = { codegen-units = 1 }
[profile.release]
debug = "limited"
debug = "full"
lto = "thin"
codegen-units = 1

View File

@@ -17830,11 +17830,6 @@ async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppCon
)
.await;
cx.update_buffer(|buffer, _| {
// This causes autoindent to be async.
buffer.set_sync_parse_timeout(Duration::ZERO)
});
cx.set_state("fn c() {\n d()ˇ\n}\n");
cx.simulate_keystroke("\n");
cx.run_until_parked();

View File

@@ -7,12 +7,14 @@ use crate::{
use anyhow::{Context as _, Result, anyhow};
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::{HashMap, HashSet};
use db::smol::stream::StreamExt;
use editor::{
Addon, Editor, EditorEvent, SelectionEffects,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
};
use futures::{FutureExt, stream::FuturesUnordered};
use git::{
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus},
@@ -27,7 +29,7 @@ use multi_buffer::{MultiBuffer, PathKey};
use project::{
Project, ProjectPath,
git_store::{
Repository,
self, Repository,
branch_diff::{self, BranchDiffEvent, DiffBase},
},
};
@@ -37,7 +39,7 @@ use std::ops::Range;
use std::sync::Arc;
use theme::ActiveTheme;
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::{ResultExt as _, rel_path::RelPath};
use util::{ResultExt, rel_path::RelPath};
use workspace::{
CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace,
@@ -551,7 +553,7 @@ impl ProjectDiff {
}
}
pub async fn refresh(this: WeakEntity<Self>, cx: &mut AsyncWindowContext) -> Result<()> {
pub async fn old_refresh(this: WeakEntity<Self>, cx: &mut AsyncWindowContext) -> Result<()> {
let mut path_keys = Vec::new();
let buffers_to_load = this.update(cx, |this, cx| {
let (repo, buffers_to_load) = this.branch_diff.update(cx, |branch_diff, cx| {
@@ -600,6 +602,98 @@ impl ProjectDiff {
Ok(())
}
pub async fn refresh(this: WeakEntity<Self>, cx: &mut AsyncWindowContext) -> Result<()> {
use git_store::branch_diff::BranchDiff;
let Some(this) = this.upgrade() else {
return Ok(());
};
let multibuffer = cx.read_entity(&this, |this, _| this.multibuffer.clone())?;
let branch_diff = cx.read_entity(&this, |pd, _| pd.branch_diff.clone())?;
let Some(repo) = cx.read_entity(&branch_diff, |bd, _| bd.repo.clone())? else {
return Ok(());
};
let project = cx.read_entity(&branch_diff, |bd, _| bd.project.clone())?;
let cached_status: Vec<git_store::StatusEntry> =
cx.read_entity(&repo, |repo: &Repository, _| repo.cached_status().collect())?;
let mut previous_paths =
cx.read_entity(&multibuffer, |mb, _| mb.paths().collect::<HashSet<_>>())?;
// Idea: on click in git panel prioritize task for that file in some way ...
// could have a hashmap of futures here
// - needs to prioritize *some* background tasks over others
// -
let mut tasks = FuturesUnordered::new();
let mut seen = HashSet::default();
for entry in cached_status {
seen.insert(entry.repo_path.clone());
let tree_diff_status = cx.read_entity(&branch_diff, |branch_diff, _| {
branch_diff
.tree_diff
.as_ref()
.and_then(|t| t.entries.get(&entry.repo_path))
.cloned()
})?;
let Some(status) = cx.read_entity(&branch_diff, |bd, _| {
bd.merge_statuses(Some(entry.status), tree_diff_status.as_ref())
})?
else {
continue;
};
if !status.has_changes() {
continue;
}
let Some(project_path) = cx.read_entity(&repo, |repo, cx| {
repo.repo_path_to_project_path(&entry.repo_path, cx)
})?
else {
continue;
};
let sort_prefix = cx.read_entity(&repo, |repo, cx| {
sort_prefix(repo, &entry.repo_path, entry.status, cx)
})?;
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.into_arc());
previous_paths.remove(&path_key);
let repo = repo.clone();
let task = project.update(cx, move |_, cx| {
BranchDiff::load_buffer(tree_diff_status, project_path, repo, cx)
.map(move |res| (res, path_key, entry.status))
})?;
tasks.push(task)
}
// remove anything not part of the diff in the multibuffer
this.update(cx, |this, cx| {
multibuffer.update(cx, |multibuffer, cx| {
for path in previous_paths {
this.buffer_diff_subscriptions.remove(&path.path);
multibuffer.remove_excerpts_for_path(path, cx);
}
});
})?;
// add the new buffers as they are parsed
while let Some((res, path_key, file_status)) = tasks.next().await {
if let Some((buffer, diff)) = res.log_err() {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.register_buffer(path_key, file_status, buffer, diff, window, cx)
});
})?;
}
}
Ok(())
}
#[cfg(any(test, feature = "test-support"))]
pub fn excerpt_paths(&self, cx: &App) -> Vec<std::sync::Arc<util::rel_path::RelPath>> {
self.multibuffer

View File

@@ -107,7 +107,6 @@ pub struct Buffer {
autoindent_requests: Vec<Arc<AutoindentRequest>>,
wait_for_autoindent_txs: Vec<oneshot::Sender<()>>,
pending_autoindent: Option<Task<()>>,
sync_parse_timeout: Duration,
syntax_map: Mutex<SyntaxMap>,
reparse: Option<Task<()>>,
parse_status: (watch::Sender<ParseStatus>, watch::Receiver<ParseStatus>),
@@ -989,7 +988,6 @@ impl Buffer {
syntax_map,
reparse: None,
non_text_state_update_count: 0,
sync_parse_timeout: Duration::from_millis(1),
parse_status: watch::channel(ParseStatus::Idle),
autoindent_requests: Default::default(),
wait_for_autoindent_txs: Default::default(),
@@ -1509,11 +1507,6 @@ impl Buffer {
self.syntax_map.lock().contains_unknown_injections()
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_sync_parse_timeout(&mut self, timeout: Duration) {
self.sync_parse_timeout = timeout;
}
/// Called after an edit to synchronize the buffer's main parse tree with
/// the buffer's new underlying state.
///
@@ -1566,43 +1559,32 @@ impl Buffer {
});
self.parse_status.0.send(ParseStatus::Parsing).unwrap();
match cx
.background_executor()
.block_with_timeout(self.sync_parse_timeout, parse_task)
{
Ok(new_syntax_snapshot) => {
self.did_finish_parsing(new_syntax_snapshot, cx);
self.reparse = None;
}
Err(parse_task) => {
// todo(lw): hot foreground spawn
self.reparse = Some(cx.spawn(async move |this, cx| {
let new_syntax_map = cx.background_spawn(parse_task).await;
this.update(cx, move |this, cx| {
let grammar_changed = || {
this.language.as_ref().is_none_or(|current_language| {
!Arc::ptr_eq(&language, current_language)
})
};
let language_registry_changed = || {
new_syntax_map.contains_unknown_injections()
&& language_registry.is_some_and(|registry| {
registry.version() != new_syntax_map.language_registry_version()
})
};
let parse_again = this.version.changed_since(&parsed_version)
|| language_registry_changed()
|| grammar_changed();
this.did_finish_parsing(new_syntax_map, cx);
this.reparse = None;
if parse_again {
this.reparse(cx);
}
})
.ok();
}));
}
}
// todo(lw): hot foreground spawn
self.reparse = Some(cx.spawn(async move |this, cx| {
let new_syntax_map = cx.background_spawn(parse_task).await;
this.update(cx, move |this, cx| {
let grammar_changed = || {
this.language
.as_ref()
.is_none_or(|current_language| !Arc::ptr_eq(&language, current_language))
};
let language_registry_changed = || {
new_syntax_map.contains_unknown_injections()
&& language_registry.is_some_and(|registry| {
registry.version() != new_syntax_map.language_registry_version()
})
};
let parse_again = this.version.changed_since(&parsed_version)
|| language_registry_changed()
|| grammar_changed();
this.did_finish_parsing(new_syntax_map, cx);
this.reparse = None;
if parse_again {
this.reparse(cx);
}
})
.ok();
}));
}
fn did_finish_parsing(&mut self, syntax_snapshot: SyntaxSnapshot, cx: &mut Context<Self>) {

View File

@@ -622,10 +622,6 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
)
);
buffer.update(cx, |buffer, _| {
buffer.set_sync_parse_timeout(Duration::ZERO)
});
// Perform some edits (add parameter and variable reference)
// Parsing doesn't begin until the transaction is complete
buffer.update(cx, |buf, cx| {
@@ -734,11 +730,7 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local("{}", cx).with_language(Arc::new(rust_lang()), cx);
buffer.set_sync_parse_timeout(Duration::ZERO);
buffer
});
let buffer = cx.new(|cx| Buffer::local("{}", cx).with_language(Arc::new(rust_lang()), cx));
// Wait for the initial text to parse
cx.executor().run_until_parked();
@@ -2216,9 +2208,6 @@ async fn test_async_autoindents_preserve_preview(cx: &mut TestAppContext) {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
// This causes autoindent to be async.
buffer.set_sync_parse_timeout(Duration::ZERO);
buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
buffer.refresh_preview();

View File

@@ -1306,6 +1306,7 @@ fn parse_text(
parser.set_included_ranges(ranges)?;
parser.set_language(&grammar.ts_language)?;
parser
// TODO JK hotspot
.parse_with_options(
&mut move |offset, _| {
chunks.seek(start_byte + offset);

View File

@@ -18,6 +18,7 @@ workspace.workspace = true
util.workspace = true
serde_json.workspace = true
smol.workspace = true
log.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -6,9 +6,9 @@ use std::{
use gpui::{
App, AppContext, Context, Entity, Hsla, InteractiveElement, IntoElement, ParentElement, Render,
ScrollHandle, SerializedTaskTiming, StatefulInteractiveElement, Styled, Task, TaskTiming,
TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, div, prelude::FluentBuilder, px,
relative, size,
ScrollHandle, SerializedThreadTaskTimings, StatefulInteractiveElement, Styled, Task,
TaskTiming, ThreadTaskTimings, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, div,
prelude::FluentBuilder, px, relative, size,
};
use util::ResultExt;
use workspace::{
@@ -287,8 +287,17 @@ impl Render for ProfilerWindow {
let Some(data) = this.get_timings() else {
return;
};
let timings = ThreadTaskTimings {
thread_name: Some("main".to_string()),
thread_id: std::thread::current().id(),
timings: data.clone(),
};
let timings =
SerializedTaskTiming::convert(this.startup_time, &data);
Vec::from([SerializedThreadTaskTimings::convert(
this.startup_time,
timings,
)]);
let active_path = workspace
.read_with(cx, |workspace, cx| {
@@ -305,12 +314,17 @@ impl Render for ProfilerWindow {
);
cx.background_spawn(async move {
let path = path.await;
let path =
path.log_err().and_then(|p| p.log_err()).flatten();
let Some(path) = path else {
return;
let path = match path.await.log_err() {
Some(Ok(Some(path))) => path,
Some(e @ Err(_)) => {
e.log_err();
log::warn!("Saving miniprof in workingdir");
std::path::Path::new(
"performance_profile.miniprof",
)
.to_path_buf()
}
Some(Ok(None)) | None => return,
};
let Some(timings) =

View File

@@ -34,11 +34,11 @@ impl DiffBase {
pub struct BranchDiff {
diff_base: DiffBase,
repo: Option<Entity<Repository>>,
project: Entity<Project>,
pub repo: Option<Entity<Repository>>,
pub project: Entity<Project>,
base_commit: Option<SharedString>,
head_commit: Option<SharedString>,
tree_diff: Option<TreeDiff>,
pub tree_diff: Option<TreeDiff>,
_subscription: Subscription,
update_needed: postage::watch::Sender<()>,
_task: Task<()>,
@@ -318,7 +318,7 @@ impl BranchDiff {
output
}
fn load_buffer(
pub fn load_buffer(
branch_diff: Option<git::status::TreeDiffStatus>,
project_path: crate::ProjectPath,
repo: Entity<Repository>,

View File

@@ -13,6 +13,32 @@ use std::{
time::Duration,
};
// https://docs.rs/tokio/latest/src/tokio/task/yield_now.rs.html#39-64
pub async fn yield_now() {
/// Yield implementation
struct YieldNow {
yielded: bool,
}
impl Future for YieldNow {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
// use core::task::ready;
// ready!(crate::trace::trace_leaf(cx));
if self.yielded {
return Poll::Ready(());
}
self.yielded = true;
// context::defer(cx.waker());
Poll::Pending
}
}
YieldNow { yielded: false }.await;
}
#[derive(Clone)]
pub struct ForegroundExecutor {
session_id: SessionId,