Avoid unnecessary DB writes (#29417)

Part of https://github.com/zed-industries/zed/issues/16472

* Adds debug logging to everywhere near INSERT/UPDATEs in the DB

So something like 
`env RUST_LOG=debug,wasmtime_cranelift=off,cranelift_codegen=off,vte=off
cargo run` could be used to view these (current zlog seems to process
the exclusions odd, so not sure this is the optimal RUST_LOG line) can
be used to debug any further writes.

* Removes excessive window stack serialization

Previously, it serialized unconditionally every 100ms.
Now, only if the stack had changed, which is now check every 500ms.

* Removes excessive terminal serialization

Previously, it serialized its `cwd` on every `ItemEvent::UpdateTab`
which was caused by e.g. any character output.
Now, only if the `cwd` has changed at the next event processing time.

Release Notes:

- Fixed more excessive DB writes
This commit is contained in:
Kirill Bulatov 2025-04-25 17:41:49 +03:00 committed by GitHub
parent 37fa437990
commit f106dfca42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 224 additions and 168 deletions

3
Cargo.lock generated
View file

@ -3158,6 +3158,7 @@ dependencies = [
"go_to_line",
"gpui",
"language",
"log",
"menu",
"picker",
"postage",
@ -3208,6 +3209,7 @@ dependencies = [
"db",
"gpui",
"languages",
"log",
"notifications",
"project",
"serde",
@ -7100,6 +7102,7 @@ dependencies = [
"editor",
"file_icons",
"gpui",
"log",
"project",
"schemars",
"serde",

View file

@ -20,6 +20,7 @@ command_palette_hooks.workspace = true
db.workspace = true
fuzzy.workspace = true
gpui.workspace = true
log.workspace = true
picker.workspace = true
postage.workspace = true
serde.workspace = true

View file

@ -67,7 +67,12 @@ impl CommandPaletteDB {
command_name: impl Into<String>,
user_query: impl Into<String>,
) -> Result<()> {
self.write_command_invocation_internal(command_name.into(), user_query.into())
let command_name = command_name.into();
let user_query = user_query.into();
log::debug!(
"Writing command invocation: command_name={command_name}, user_query={user_query}"
);
self.write_command_invocation_internal(command_name, user_query)
.await
}

View file

@ -21,6 +21,7 @@ component.workspace = true
gpui.workspace = true
languages.workspace = true
notifications.workspace = true
log.workspace = true
project.workspace = true
ui.workspace = true
ui_input.workspace = true

View file

@ -23,6 +23,9 @@ impl ComponentPreviewDb {
workspace_id: WorkspaceId,
active_page_id: String,
) -> Result<()> {
log::debug!(
"Saving active page: item_id={item_id:?}, workspace_id={workspace_id:?}, active_page_id={active_page_id}"
);
let query = "INSERT INTO component_previews(item_id, workspace_id, active_page_id)
VALUES (?1, ?2, ?3)
ON CONFLICT DO UPDATE SET

View file

@ -18,8 +18,13 @@ impl KeyValueStore {
}
}
pub async fn write_kvp(&self, key: String, value: String) -> anyhow::Result<()> {
log::debug!("Writing key-value pair for key {key}");
self.write_kvp_inner(key, value).await
}
query! {
pub async fn write_kvp(key: String, value: String) -> Result<()> {
async fn write_kvp_inner(key: String, value: String) -> Result<()> {
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
}
}
@ -78,8 +83,13 @@ impl GlobalKeyValueStore {
}
}
pub async fn write_kvp(&self, key: String, value: String) -> anyhow::Result<()> {
log::debug!("Writing global key-value pair for key {key}");
self.write_kvp_inner(key, value).await
}
query! {
pub async fn write_kvp(key: String, value: String) -> Result<()> {
async fn write_kvp_inner(key: String, value: String) -> Result<()> {
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
}
}

View file

@ -1251,6 +1251,7 @@ impl SerializableItem for Editor {
language,
mtime,
};
log::debug!("Serializing editor {item_id:?} in workspace {workspace_id:?}");
DB.save_serialized_editor(item_id, workspace_id, editor)
.await
.context("failed to save serialized editor")

View file

@ -276,6 +276,7 @@ impl EditorDb {
workspace_id: WorkspaceId,
selections: Vec<(usize, usize)>,
) -> Result<()> {
log::debug!("Saving selections for editor {editor_id} in workspace {workspace_id:?}");
let mut first_selection;
let mut last_selection = 0_usize;
for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
@ -327,6 +328,7 @@ VALUES {placeholders};
workspace_id: WorkspaceId,
folds: Vec<(usize, usize)>,
) -> Result<()> {
log::debug!("Saving folds for editor {editor_id} in workspace {workspace_id:?}");
let mut first_fold;
let mut last_fold = 0_usize;
for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")

View file

@ -270,6 +270,9 @@ impl ScrollManager {
cx.foreground_executor()
.spawn(async move {
log::debug!(
"Saving scroll position for item {item_id:?} in workspace {workspace_id:?}"
);
DB.save_scroll_position(
item_id,
workspace_id,

View file

@ -21,6 +21,7 @@ db.workspace = true
editor.workspace = true
file_icons.workspace = true
gpui.workspace = true
log.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -261,6 +261,7 @@ impl SerializableItem for ImageView {
Some(cx.background_spawn({
async move {
log::debug!("Saving image at path {image_path:?}");
IMAGE_VIEWER
.save_image_path(item_id, workspace_id, image_path)
.await
@ -399,18 +400,6 @@ mod persistence {
}
impl ImageViewerDb {
query! {
pub async fn update_workspace_id(
new_id: WorkspaceId,
old_id: WorkspaceId,
item_id: ItemId
) -> Result<()> {
UPDATE image_viewers
SET workspace_id = ?
WHERE workspace_id = ? AND item_id = ?
}
}
query! {
pub async fn save_image_path(
item_id: ItemId,

View file

@ -1,7 +1,7 @@
use std::time::Duration;
use db::kvp::KEY_VALUE_STORE;
use gpui::{AnyWindowHandle, AppContext as _, Context, Subscription, Task, WindowId};
use gpui::{App, AppContext as _, Context, Subscription, Task, WindowId};
use util::ResultExt;
use uuid::Uuid;
@ -59,7 +59,7 @@ impl Session {
pub struct AppSession {
session: Session,
_serialization_task: Option<Task<()>>,
_serialization_task: Task<()>,
_subscriptions: Vec<Subscription>,
}
@ -67,17 +67,21 @@ impl AppSession {
pub fn new(session: Session, cx: &Context<Self>) -> Self {
let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)];
let _serialization_task = Some(cx.spawn(async move |_, cx| {
let _serialization_task = cx.spawn(async move |_, cx| {
let mut current_window_stack = Vec::new();
loop {
if let Some(windows) = cx.update(|cx| cx.window_stack()).ok().flatten() {
store_window_stack(windows).await;
if let Some(windows) = cx.update(|cx| window_stack(cx)).ok().flatten() {
if windows != current_window_stack {
store_window_stack(&windows).await;
current_window_stack = windows;
}
}
cx.background_executor()
.timer(Duration::from_millis(100))
.timer(Duration::from_millis(500))
.await;
}
}));
});
Self {
session,
@ -87,8 +91,8 @@ impl AppSession {
}
fn app_will_quit(&mut self, cx: &mut Context<Self>) -> Task<()> {
if let Some(windows) = cx.window_stack() {
cx.background_spawn(store_window_stack(windows))
if let Some(window_stack) = window_stack(cx) {
cx.background_spawn(async move { store_window_stack(&window_stack).await })
} else {
Task::ready(())
}
@ -107,13 +111,17 @@ impl AppSession {
}
}
async fn store_window_stack(windows: Vec<AnyWindowHandle>) {
let window_ids = windows
.into_iter()
.map(|window| window.window_id().as_u64())
.collect::<Vec<_>>();
fn window_stack(cx: &App) -> Option<Vec<u64>> {
Some(
cx.window_stack()?
.into_iter()
.map(|window| window.window_id().as_u64())
.collect(),
)
}
if let Ok(window_ids_json) = serde_json::to_string(&window_ids) {
async fn store_window_stack(windows: &[u64]) {
if let Ok(window_ids_json) = serde_json::to_string(windows) {
KEY_VALUE_STORE
.write_kvp(SESSION_WINDOW_STACK_KEY.to_string(), window_ids_json)
.await

View file

@ -429,6 +429,9 @@ impl TerminalDb {
workspace_id: WorkspaceId,
working_directory: PathBuf,
) -> Result<()> {
log::debug!(
"Saving working directory {working_directory:?} for item {item_id} in workspace {workspace_id:?}"
);
let query =
"INSERT INTO terminals(item_id, workspace_id, working_directory, working_directory_path)
VALUES (?1, ?2, ?3, ?4)

View file

@ -112,6 +112,7 @@ pub struct TerminalView {
cursor_shape: CursorShape,
blink_state: bool,
blinking_terminal_enabled: bool,
cwd_serialized: bool,
blinking_paused: bool,
blink_epoch: usize,
hover_target_tooltip: Option<String>,
@ -207,6 +208,7 @@ impl TerminalView {
scroll_handle,
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
cwd_serialized: false,
_subscriptions: vec![
focus_in,
focus_out,
@ -843,167 +845,176 @@ fn subscribe_for_terminal_events(
cx: &mut Context<TerminalView>,
) -> Vec<Subscription> {
let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify());
let mut previous_cwd = None;
let terminal_events_subscription = cx.subscribe_in(
terminal,
window,
move |terminal_view, _, event, window, cx| match event {
Event::Wakeup => {
cx.notify();
cx.emit(Event::Wakeup);
cx.emit(ItemEvent::UpdateTab);
cx.emit(SearchEvent::MatchesInvalidated);
move |terminal_view, terminal, event, window, cx| {
let current_cwd = terminal.read(cx).working_directory();
if current_cwd != previous_cwd {
previous_cwd = current_cwd;
terminal_view.cwd_serialized = false;
}
Event::Bell => {
terminal_view.has_bell = true;
cx.emit(Event::Wakeup);
}
Event::BlinkChanged(blinking) => {
if matches!(
TerminalSettings::get_global(cx).blinking,
TerminalBlink::TerminalControlled
) {
terminal_view.blinking_terminal_enabled = *blinking;
match event {
Event::Wakeup => {
cx.notify();
cx.emit(Event::Wakeup);
cx.emit(ItemEvent::UpdateTab);
cx.emit(SearchEvent::MatchesInvalidated);
}
}
Event::TitleChanged => {
cx.emit(ItemEvent::UpdateTab);
}
Event::Bell => {
terminal_view.has_bell = true;
cx.emit(Event::Wakeup);
}
Event::NewNavigationTarget(maybe_navigation_target) => {
match maybe_navigation_target.as_ref() {
None => {
terminal_view.hover_target_tooltip = None;
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::Url(url)) => {
terminal_view.hover_target_tooltip = Some(url.clone());
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::PathLike(path_like_target)) => {
let valid_files_to_open_task = possible_open_target(
&workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
);
terminal_view.hover_tooltip_update =
cx.spawn(async move |terminal_view, cx| {
let file_to_open = valid_files_to_open_task.await;
terminal_view
.update(cx, |terminal_view, _| match file_to_open {
Some(
OpenTarget::File(path, _)
| OpenTarget::Worktree(path, _),
) => {
terminal_view.hover_target_tooltip =
Some(path.to_string(|path| {
path.to_string_lossy().to_string()
}));
}
None => {
terminal_view.hover_target_tooltip = None;
}
})
.ok();
});
Event::BlinkChanged(blinking) => {
if matches!(
TerminalSettings::get_global(cx).blinking,
TerminalBlink::TerminalControlled
) {
terminal_view.blinking_terminal_enabled = *blinking;
}
}
cx.notify()
}
Event::TitleChanged => {
cx.emit(ItemEvent::UpdateTab);
}
Event::Open(maybe_navigation_target) => match maybe_navigation_target {
MaybeNavigationTarget::Url(url) => cx.open_url(url),
Event::NewNavigationTarget(maybe_navigation_target) => {
match maybe_navigation_target.as_ref() {
None => {
terminal_view.hover_target_tooltip = None;
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::Url(url)) => {
terminal_view.hover_target_tooltip = Some(url.clone());
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::PathLike(path_like_target)) => {
let valid_files_to_open_task = possible_open_target(
&workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
);
MaybeNavigationTarget::PathLike(path_like_target) => {
if terminal_view.hover_target_tooltip.is_none() {
return;
terminal_view.hover_tooltip_update =
cx.spawn(async move |terminal_view, cx| {
let file_to_open = valid_files_to_open_task.await;
terminal_view
.update(cx, |terminal_view, _| match file_to_open {
Some(
OpenTarget::File(path, _)
| OpenTarget::Worktree(path, _),
) => {
terminal_view.hover_target_tooltip =
Some(path.to_string(|path| {
path.to_string_lossy().to_string()
}));
}
None => {
terminal_view.hover_target_tooltip = None;
}
})
.ok();
});
}
}
let task_workspace = workspace.clone();
let path_like_target = path_like_target.clone();
cx.spawn_in(window, async move |terminal_view, cx| {
let open_target = terminal_view
.update(cx, |_, cx| {
possible_open_target(
&task_workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
)
})?
.await;
if let Some(open_target) = open_target {
let path_to_open = open_target.path();
let opened_items = task_workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_paths(
vec![path_to_open.path.clone()],
OpenOptions {
visible: Some(OpenVisible::OnlyDirectories),
..Default::default()
},
None,
window,
cx.notify()
}
Event::Open(maybe_navigation_target) => match maybe_navigation_target {
MaybeNavigationTarget::Url(url) => cx.open_url(url),
MaybeNavigationTarget::PathLike(path_like_target) => {
if terminal_view.hover_target_tooltip.is_none() {
return;
}
let task_workspace = workspace.clone();
let path_like_target = path_like_target.clone();
cx.spawn_in(window, async move |terminal_view, cx| {
let open_target = terminal_view
.update(cx, |_, cx| {
possible_open_target(
&task_workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
)
})
.context("workspace update")?
})?
.await;
if opened_items.len() != 1 {
debug_panic!(
"Received {} items for one path {path_to_open:?}",
opened_items.len(),
);
}
if let Some(open_target) = open_target {
let path_to_open = open_target.path();
let opened_items = task_workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_paths(
vec![path_to_open.path.clone()],
OpenOptions {
visible: Some(OpenVisible::OnlyDirectories),
..Default::default()
},
None,
window,
cx,
)
})
.context("workspace update")?
.await;
if opened_items.len() != 1 {
debug_panic!(
"Received {} items for one path {path_to_open:?}",
opened_items.len(),
);
}
if let Some(opened_item) = opened_items.first() {
if open_target.is_file() {
if let Some(Ok(opened_item)) = opened_item {
if let Some(row) = path_to_open.row {
let col = path_to_open.column.unwrap_or(0);
if let Some(active_editor) =
opened_item.downcast::<Editor>()
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
language::Point::new(
row.saturating_sub(1),
col.saturating_sub(1),
),
window,
cx,
)
})
.log_err();
if let Some(opened_item) = opened_items.first() {
if open_target.is_file() {
if let Some(Ok(opened_item)) = opened_item {
if let Some(row) = path_to_open.row {
let col = path_to_open.column.unwrap_or(0);
if let Some(active_editor) =
opened_item.downcast::<Editor>()
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
language::Point::new(
row.saturating_sub(1),
col.saturating_sub(1),
),
window,
cx,
)
})
.log_err();
}
}
}
} else if open_target.is_dir() {
task_workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |_, cx| {
cx.emit(project::Event::ActivateProjectPanel);
})
})?;
}
} else if open_target.is_dir() {
task_workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |_, cx| {
cx.emit(project::Event::ActivateProjectPanel);
})
})?;
}
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx)
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
},
Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
Event::SelectionsChanged => {
window.invalidate_character_coordinates();
cx.emit(SearchEvent::ActiveMatchChanged)
}
},
Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
Event::SelectionsChanged => {
window.invalidate_character_coordinates();
cx.emit(SearchEvent::ActiveMatchChanged)
}
},
);
@ -1539,6 +1550,9 @@ impl Item for TerminalView {
) {
if self.terminal().read(cx).task().is_none() {
if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
log::debug!(
"Updating workspace id for the terminal, old: {old_id:?}, new: {new_id:?}",
);
cx.background_spawn(TERMINAL_DB.update_workspace_id(
new_id,
old_id,
@ -1587,6 +1601,7 @@ impl SerializableItem for TerminalView {
}
if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) {
self.cwd_serialized = true;
Some(cx.background_spawn(async move {
TERMINAL_DB
.save_working_directory(item_id, workspace_id, cwd)
@ -1597,8 +1612,8 @@ impl SerializableItem for TerminalView {
}
}
fn should_serialize(&self, event: &Self::Event) -> bool {
matches!(event, ItemEvent::UpdateTab)
fn should_serialize(&self, _: &Self::Event) -> bool {
!self.cwd_serialized
}
fn deserialize(

View file

@ -1644,6 +1644,7 @@ impl VimDb {
path: Arc<Path>,
marks: HashMap<String, Vec<Point>>,
) -> Result<()> {
log::debug!("Setting path {path:?} for {} marks", marks.len());
let result = self
.write(move |conn| {
let mut query = conn.exec_bound(sql!(
@ -1694,6 +1695,7 @@ impl VimDb {
mark_name: String,
path: Arc<Path>,
) -> Result<()> {
log::debug!("Setting global mark path {path:?} for {mark_name}");
self.write(move |conn| {
conn.exec_bound(sql!(
INSERT OR REPLACE INTO vim_global_marks_paths

View file

@ -739,6 +739,7 @@ impl WorkspaceDb {
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
/// that used this workspace previously
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
log::debug!("Saving workspace at location: {:?}", workspace.location);
self.write(move |conn| {
conn.with_savepoint("update_worktrees", || {
// Clear out panes and pane_groups
@ -909,6 +910,7 @@ impl WorkspaceDb {
{
Ok(project)
} else {
log::debug!("Inserting SSH project at host {host}");
self.insert_ssh_project(host, port, paths, user)
.await?
.ok_or_else(|| anyhow!("failed to insert ssh project"))
@ -1209,6 +1211,9 @@ impl WorkspaceDb {
pane_group: &SerializedPaneGroup,
parent: Option<(GroupId, usize)>,
) -> Result<()> {
if parent.is_none() {
log::debug!("Saving a pane group for workspace {workspace_id:?}");
}
match pane_group {
SerializedPaneGroup::Group {
axis,
@ -1387,6 +1392,10 @@ impl WorkspaceDb {
relative_worktree_path: String,
toolchain: Toolchain,
) -> Result<()> {
log::debug!(
"Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
toolchain.name
);
self.write(move |conn| {
let mut insert = conn
.exec_bound(sql!(