Fix local filetree page tree open performance

This commit is contained in:
lix-2026
2026-05-27 12:53:55 +08:00
parent 3ae33cc21d
commit c01dc2dea2
10 changed files with 730 additions and 47 deletions
+53 -10
View File
@@ -14,6 +14,7 @@ use crate::routes::web_shell::{
render_document_title_controller_script, render_editor_island_adapter_script,
render_editor_runtime_preload_links, render_local_file_tree_html,
render_local_file_tree_html_scoped, render_local_sidebar_tree_html,
render_local_sidebar_tree_html_from_snapshot,
};
use crate::transport::convex::execute_convex_mutation_by_name;
use crate::workspace_shell::{
@@ -359,8 +360,10 @@ pub async fn root_entry(
.first()
.map(|item| item.id.as_str()),
);
let sidebar_tree_html =
render_local_sidebar_tree_html(root_uri, selected_active_page_id.as_deref())?;
let sidebar_tree_html = render_local_sidebar_tree_html_from_snapshot(
&page_tree_snapshot,
selected_active_page_id.as_deref(),
);
let restore_focus_row_id = query
.restore_focus_row_id
.as_deref()
@@ -426,8 +429,10 @@ pub async fn root_entry(
.first()
.map(|item| item.id.as_str()),
);
let sidebar_tree_html =
render_local_sidebar_tree_html(&root_uri, selected_active_page_id.as_deref())?;
let sidebar_tree_html = render_local_sidebar_tree_html_from_snapshot(
&snapshot,
selected_active_page_id.as_deref(),
);
let file_tree_html =
render_local_file_tree_html(&root_uri, selected_active_page_id.as_deref(), None)?;
(
@@ -607,12 +612,12 @@ pub async fn root_entry(
Err(_) => ("MNOTE".to_string(), render_workspace_entry(), String::new()),
}
};
let editor_runtime_preload_links =
if body_extra.contains("document-editor-adapter-runtime.js") {
render_editor_runtime_preload_links()
} else {
""
};
let editor_runtime_preload_links = if body_extra.contains("document-editor-adapter-runtime.js")
{
render_editor_runtime_preload_links()
} else {
""
};
let mut response = Html(format!(
r#"<!doctype html>
<html lang="zh-CN">
@@ -2817,6 +2822,44 @@ mod tests {
assert!(!html.contains(r#"href="/tree"#));
}
#[tokio::test]
async fn root_entry_local_folder_reuses_page_tree_snapshot_for_sidebar_html() {
let root = temp_root("mnote-root-local-folder-page-tree-snapshot-once");
std::fs::create_dir_all(root.join("docs")).expect("create local docs");
std::fs::write(root.join("README.md"), "# Local Root\n正文\n").expect("write root md");
std::fs::write(root.join("docs").join("child.md"), "# Child\n").expect("write child md");
let root_uri = format!("file://{}", root.display());
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
"user_real",
&root_uri,
)
.expect("init local workspace");
crate::routes::local_folder_source::reset_local_page_tree_snapshot_test_loads();
let response = app_with_config("http://127.0.0.1:3100".into(), false)
.oneshot(
Request::builder()
.uri(format!(
"/?sourceKind=local_folder&rootUri={root_uri}&treeView=filetree"
))
.header("x-mnote-actor-id", "user_real")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
let _ = std::fs::remove_dir_all(&root);
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
crate::routes::local_folder_source::local_page_tree_snapshot_test_loads(),
1,
"root entry should reuse the already loaded local page tree snapshot instead of scanning it twice",
);
}
#[tokio::test]
async fn root_entry_local_folder_without_active_page_keeps_resource_tab_host() {
let root = temp_root("mnote-root-local-folder-no-active-page");
+10 -2
View File
@@ -125,7 +125,9 @@ async fn project_projection(
}
})
.await
.map_err(|error| WebError::internal(format!("本地树 projection 构建任务失败: {error}")))??;
.map_err(|error| {
WebError::internal(format!("本地树 projection 构建任务失败: {error}"))
})??;
return Ok(ok_response(&context, snapshot.projection));
}
@@ -409,7 +411,13 @@ mod tests {
.as_array()
.expect("items")
.iter()
.any(|item| item["title"] == "本地页面.md"));
.any(|item| item["title"] == "本地页面"
&& item["rowKind"] == "folder"
&& item["documentId"]
.as_str()
.unwrap_or("")
.starts_with("local-md:")
&& item["resourceMeta"]["extra"]["source"]["relativePath"] == "本地页面"));
let _ = std::fs::remove_dir_all(&root);
}
@@ -31,9 +31,53 @@ use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use time::OffsetDateTime;
#[cfg(test)]
static LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[cfg(test)]
static LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[derive(Debug, Clone)]
struct LocalPageTreeSnapshotCacheEntry {
watch_revision: LocalFolderWatchRevision,
snapshot: ProjectionSnapshot,
}
static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
> = OnceLock::new();
fn local_page_tree_snapshot_cache(
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
}
#[cfg(test)]
pub(crate) fn reset_local_page_tree_snapshot_test_loads() {
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
if let Some(cache) = LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get() {
if let Ok(mut cache) = cache.lock() {
cache.clear();
}
}
}
#[cfg(test)]
pub(crate) fn local_page_tree_snapshot_test_loads() -> u64 {
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
}
#[cfg(test)]
fn local_page_tree_snapshot_scan_test_loads() -> u64 {
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
}
#[derive(Debug, Clone)]
struct LocalFolderEntry {
path: PathBuf,
@@ -2724,6 +2768,9 @@ fn load_local_folder_file_tree_scope_snapshot(
pub fn load_local_folder_page_tree_snapshot(
root_uri: &str,
) -> Result<ProjectionSnapshot, WebError> {
#[cfg(test)]
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let root_path = parse_file_root_uri(root_uri)?;
let canonical_root = root_path.canonicalize().map_err(|error| {
WebError::bad_request_code(
@@ -2740,8 +2787,21 @@ pub fn load_local_folder_page_tree_snapshot(
let workspace_id = local_workspace_id(&canonical_root);
let root_source_uri = file_uri_for_path(&canonical_root);
let watch_revision = local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?;
if let Ok(cache) = local_page_tree_snapshot_cache().lock() {
if let Some(entry) = cache.get(&root_source_uri) {
if entry.watch_revision.revision == watch_revision.revision
&& entry.watch_revision.entry_count == watch_revision.entry_count
&& entry.watch_revision.latest_modified_ms == watch_revision.latest_modified_ms
{
return Ok(entry.snapshot.clone());
}
}
}
let metadata = load_local_folder_metadata(&canonical_root)?;
let mut rows = Vec::new();
#[cfg(test)]
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
scan_markdown_page_tree(
&canonical_root,
&canonical_root,
@@ -2773,8 +2833,7 @@ pub fn load_local_folder_page_tree_snapshot(
.iter()
.map(local_folder_row_to_projection_item)
.collect::<Vec<_>>();
let watch_revision = local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?;
Ok(ProjectionSnapshot {
let snapshot = ProjectionSnapshot {
dataset: json!({
"workspace": {
"id": local_workspace_id(&canonical_root),
@@ -2803,7 +2862,20 @@ pub fn load_local_folder_page_tree_snapshot(
"watchRevision": watch_revision,
"items": items,
}),
})
};
if let Ok(mut cache) = local_page_tree_snapshot_cache().lock() {
if cache.len() > 64 {
cache.clear();
}
cache.insert(
root_source_uri,
LocalPageTreeSnapshotCacheEntry {
watch_revision,
snapshot: snapshot.clone(),
},
);
}
Ok(snapshot)
}
pub fn local_folder_watch_revision(root_uri: &str) -> Result<LocalFolderWatchRevision, WebError> {
@@ -9070,6 +9142,27 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn local_folder_page_tree_snapshot_reuses_cache_until_watch_revision_changes() {
let root = temp_root("mnote-page-tree-snapshot-cache");
init_workspace(&root);
std::fs::write(root.join("page.md"), "# Page\n").expect("write md");
let root_uri = format!("file://{}", root.display());
super::reset_local_page_tree_snapshot_test_loads();
let first = load_local_folder_page_tree_snapshot(&root_uri).expect("first snapshot");
let second = load_local_folder_page_tree_snapshot(&root_uri).expect("second snapshot");
assert_eq!(first.projection, second.projection);
assert_eq!(super::local_page_tree_snapshot_scan_test_loads(), 1);
std::fs::write(root.join("next.md"), "# Next\n").expect("write next");
let third = load_local_folder_page_tree_snapshot(&root_uri).expect("third snapshot");
assert_ne!(second.projection, third.projection);
assert_eq!(super::local_page_tree_snapshot_scan_test_loads(), 2);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id() {
let root = temp_root("mnote-local-frontmatter-path-id");
+16 -5
View File
@@ -1350,7 +1350,9 @@ pub(crate) async fn build_page_aggregate_snapshot(
resolve_local_markdown_page_aggregate(&root_uri_for_build, &document_id)
})
.await
.map_err(|error| WebError::internal(format!("本地 Page Aggregate 构建任务失败: {error}")))??;
.map_err(|error| {
WebError::internal(format!("本地 Page Aggregate 构建任务失败: {error}"))
})??;
super::ui_preferences::apply_effective_page_preferences(
state,
context,
@@ -1661,12 +1663,22 @@ pub(crate) fn render_local_sidebar_tree_html(
active_document_id: Option<&str>,
) -> Result<String, WebError> {
let snapshot = load_local_folder_page_tree_snapshot(root_uri)?;
Ok(render_local_sidebar_tree_html_from_snapshot(
&snapshot,
active_document_id,
))
}
pub(crate) fn render_local_sidebar_tree_html_from_snapshot(
snapshot: &crate::routes::snapshot_support::ProjectionSnapshot,
active_document_id: Option<&str>,
) -> String {
let rows = collect_page_tree_render_rows(&snapshot.projection);
Ok(render_initial_page_tree_html(&PageTreeInitialRenderInput {
render_initial_page_tree_html(&PageTreeInitialRenderInput {
rows,
active_node_id: active_document_id.map(ToOwned::to_owned),
focused_node_id: None,
}))
})
}
pub(crate) fn render_local_file_tree_html(
@@ -2436,8 +2448,7 @@ mod tests {
let aggregate_uri = format!(
"/api/page-aggregate/local-md:Cold~20Start~2FCold~20Start.md?sourceKind=local_folder&rootUri={root_uri}"
);
super::LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS
.store(400, std::sync::atomic::Ordering::SeqCst);
super::LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS.store(400, std::sync::atomic::Ordering::SeqCst);
struct ResetLocalAggregateBlock;
impl Drop for ResetLocalAggregateBlock {
fn drop(&mut self) {