Improve local filetree view state and sidebar performance

This commit is contained in:
lix-2026
2026-05-27 11:31:12 +08:00
parent 58e2fdb5d8
commit 3ae33cc21d
56 changed files with 8614 additions and 461 deletions
+108 -23
View File
@@ -19,6 +19,10 @@ use core_protocol::{KernelGraphDirection, KernelProjectionKind};
use serde::Deserialize;
use serde_json::{json, Value};
#[cfg(test)]
static LOCAL_FOLDER_PROJECTION_TEST_BLOCK_MS: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KernelProjectionQuery {
@@ -89,31 +93,39 @@ async fn project_projection(
})?;
ensure_local_workspace_read_access_with_state(&state, &context, root_uri)
.map_err(|error| error.with_context(&context))?;
let snapshot = if projection == KernelProjectionKind::FileTree {
if let Some(parent_relative_path) = query
.parent_relative_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
load_local_folder_file_tree_children_snapshot(root_uri, parent_relative_path)?
} else if query
.root_node_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_some()
{
load_local_folder_file_tree_snapshot_with_reveal(
root_uri,
query.root_node_id.as_deref(),
)?
let root_uri = root_uri.to_string();
let parent_relative_path = query
.parent_relative_path
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let root_node_id = query
.root_node_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
let snapshot = tokio::task::spawn_blocking(move || {
#[cfg(test)]
block_local_folder_projection_for_test();
if projection == KernelProjectionKind::FileTree {
if let Some(parent_relative_path) = parent_relative_path.as_deref() {
load_local_folder_file_tree_children_snapshot(&root_uri, parent_relative_path)
} else if root_node_id.is_some() {
load_local_folder_file_tree_snapshot_with_reveal(
&root_uri,
root_node_id.as_deref(),
)
} else {
load_local_folder_file_tree_snapshot(&root_uri)
}
} else {
load_local_folder_file_tree_snapshot(root_uri)?
load_local_folder_page_tree_snapshot(&root_uri)
}
} else {
load_local_folder_page_tree_snapshot(root_uri)?
};
})
.await
.map_err(|error| WebError::internal(format!("本地树 projection 构建任务失败: {error}")))??;
return Ok(ok_response(&context, snapshot.projection));
}
@@ -137,6 +149,14 @@ async fn project_projection(
Ok(ok_response(&context, snapshot.projection))
}
#[cfg(test)]
fn block_local_folder_projection_for_test() {
let delay_ms = LOCAL_FOLDER_PROJECTION_TEST_BLOCK_MS.load(std::sync::atomic::Ordering::SeqCst);
if delay_ms > 0 {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
}
pub async fn project_tree_sidebar(
state: State<AppState>,
context: Extension<RequestContext>,
@@ -245,6 +265,7 @@ mod tests {
use axum::body::{to_bytes, Body};
use axum::http::{Request, StatusCode};
use serde_json::Value;
use std::time::{Duration, Instant};
use tower::util::ServiceExt;
fn app() -> axum::Router {
@@ -443,6 +464,70 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn local_folder_file_projection_does_not_block_leptos_runtime_asset_request() {
let root = std::env::temp_dir().join(format!(
"mnote-local-kernel-projection-runtime-asset-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("docs")).expect("create docs");
std::fs::write(root.join("docs").join("README.md"), "# README\n").expect("write readme");
let root_uri = format!("file://{}", root.display());
initialize_local_workspace_for_actor("dev-user", &root_uri).expect("init local workspace");
let app = app();
let projection_app = app.clone();
let projection_uri = format!(
"/api/tree/projections/file/children?workspaceId=local-ws:dev-user:my-space&sourceKind=local_folder&rootUri={root_uri}&parentRelativePath=docs"
);
super::LOCAL_FOLDER_PROJECTION_TEST_BLOCK_MS
.store(400, std::sync::atomic::Ordering::SeqCst);
struct ResetLocalProjectionBlock;
impl Drop for ResetLocalProjectionBlock {
fn drop(&mut self) {
super::LOCAL_FOLDER_PROJECTION_TEST_BLOCK_MS
.store(0, std::sync::atomic::Ordering::SeqCst);
}
}
let _reset_block = ResetLocalProjectionBlock;
let projection_task = tokio::spawn(async move {
projection_app
.oneshot(
Request::builder()
.uri(projection_uri)
.header("x-mnote-actor-id", "dev-user")
.header("x-mnote-actor-type", "user")
.body(Body::empty())
.expect("projection request"),
)
.await
.expect("projection response")
});
let started = Instant::now();
tokio::task::yield_now().await;
let asset_response = app
.oneshot(
Request::builder()
.uri("/api/leptos-tiptap-runtime/mnote-leptos-tiptap-spike-island.js")
.body(Body::empty())
.expect("asset request"),
)
.await
.expect("asset response");
let asset_elapsed = started.elapsed();
let projection_response = projection_task.await.expect("projection task");
let _ = std::fs::remove_dir_all(&root);
assert_eq!(asset_response.status(), StatusCode::OK);
assert_eq!(projection_response.status(), StatusCode::OK);
assert!(
asset_elapsed < Duration::from_millis(150),
"leptos-tiptap runtime asset 不应被本地 FileTree projection 扫描阻塞,实际等待 {asset_elapsed:?}"
);
}
#[tokio::test]
async fn tree_projection_routes_keep_ok_response_shape() {
let response = app()