forked from 1b-agent/opencadstudio-upstream
perf: keep heavy work off UI thread
This commit is contained in:
1 parent
12bf126646
commit
deec48ad99
75 files changed
+11702
-518
No files matched your search
+149
-20
@@ -18,20 +18,58 @@ pub mod patterns;
|
||||
pub mod update_check;
|
||||
pub mod paper_sizes;
|
||||
pub mod thumbnail;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web_worker;
|
||||
|
||||
use crate::scene::DerivedCaches;
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::io::dwg::DwgReader;
|
||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Phase tags written into the shared atomic so the UI overlay can display a
|
||||
// human-readable label. Kept in sync with the constants in `crate::app`.
|
||||
const PHASE_PARSING: u8 = 1;
|
||||
const PHASE_CACHING: u8 = 2;
|
||||
const PHASE_FINALIZING: u8 = 3;
|
||||
/// Thread-safe state shared by the native loader and the open overlay.
|
||||
///
|
||||
/// `basis_points` is monotonic in 0..=10000. `completed/total` describe the
|
||||
/// current sub-stage and let diagnostics distinguish real progress from a
|
||||
/// cosmetic timer.
|
||||
#[derive(Debug)]
|
||||
pub struct OpenProgressState {
|
||||
pub phase: AtomicU8,
|
||||
pub basis_points: AtomicU16,
|
||||
pub completed: AtomicU32,
|
||||
pub total: AtomicU32,
|
||||
}
|
||||
|
||||
impl OpenProgressState {
|
||||
pub fn new(phase: u8) -> Self {
|
||||
Self {
|
||||
phase: AtomicU8::new(phase),
|
||||
basis_points: AtomicU16::new(0),
|
||||
completed: AtomicU32::new(0),
|
||||
total: AtomicU32::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&self, phase: u8, basis_points: u16, completed: usize, total: usize) {
|
||||
self.completed
|
||||
.store(completed.min(u32::MAX as usize) as u32, Ordering::Relaxed);
|
||||
self.total.store(
|
||||
total.max(1).min(u32::MAX as usize) as u32,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
self.basis_points
|
||||
.fetch_max(basis_points.min(10000), Ordering::Relaxed);
|
||||
self.phase.store(phase, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn set_fraction(&self, phase: u8, base: u16, span: u16, completed: usize, total: usize) {
|
||||
let denominator = total.max(1) as u64;
|
||||
let value = base as u64 + (completed.min(total.max(1)) as u64 * span as u64 / denominator);
|
||||
self.set(phase, value.min(10000) as u16, completed, total);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Open ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -61,37 +99,103 @@ pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
|
||||
/// thread runs.
|
||||
pub async fn open_path_with_phase(
|
||||
path: PathBuf,
|
||||
phase: Arc<AtomicU8>,
|
||||
progress: Arc<OpenProgressState>,
|
||||
model_bg: [f32; 4],
|
||||
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
let name = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
let path2 = path.clone();
|
||||
let phase2 = phase.clone();
|
||||
let (doc, caches) = std::thread::spawn(move || -> Result<_, String> {
|
||||
let progress2 = progress.clone();
|
||||
let (sender, receiver) = iced::futures::channel::oneshot::channel();
|
||||
std::thread::Builder::new()
|
||||
.name("ocs-file-open".to_string())
|
||||
.spawn(move || {
|
||||
let result = (|| -> Result<_, String> {
|
||||
use iced::time::Instant;
|
||||
phase2.store(PHASE_PARSING, Ordering::Relaxed);
|
||||
progress2.set(crate::app::OPEN_PHASE_PARSING, 200, 0, 1000);
|
||||
let t_parse = Instant::now();
|
||||
let mut doc = load_file(&path2)?;
|
||||
let parser_progress = {
|
||||
let progress = Arc::clone(&progress2);
|
||||
let callback: Arc<dyn Fn(u16) + Send + Sync> = Arc::new(move |value| {
|
||||
progress.set_fraction(
|
||||
crate::app::OPEN_PHASE_PARSING,
|
||||
200,
|
||||
5600,
|
||||
value as usize,
|
||||
1000,
|
||||
);
|
||||
});
|
||||
callback
|
||||
};
|
||||
let mut doc = load_file_with_progress(&path2, Some(parser_progress))?;
|
||||
let parse_ms = t_parse.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_PARSING, 5800, 1000, 1000);
|
||||
let t_purge = Instant::now();
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
let purge_ms = t_purge.elapsed().as_millis() as u32;
|
||||
phase2.store(PHASE_CACHING, Ordering::Relaxed);
|
||||
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
|
||||
let t_xref = Instant::now();
|
||||
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
|
||||
let xref_progress = {
|
||||
let progress = Arc::clone(&progress2);
|
||||
let callback: Arc<dyn Fn(usize, usize) + Send + Sync> =
|
||||
Arc::new(move |completed, total| {
|
||||
progress.set_fraction(
|
||||
crate::app::OPEN_PHASE_XREF,
|
||||
6000,
|
||||
1400,
|
||||
completed,
|
||||
total,
|
||||
);
|
||||
});
|
||||
callback
|
||||
};
|
||||
crate::io::xref::resolve_xrefs_with_progress(
|
||||
&mut doc,
|
||||
base_dir,
|
||||
Some(xref_progress),
|
||||
)
|
||||
} else {
|
||||
(Vec::new(), 0)
|
||||
};
|
||||
let xref_ms = t_xref.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_CACHING, 7400, 0, 10000);
|
||||
let t_caches = Instant::now();
|
||||
let mut caches = crate::scene::build_derived_caches(&doc);
|
||||
let cache_progress = |value: u16| {
|
||||
progress2.set_fraction(
|
||||
crate::app::OPEN_PHASE_CACHING,
|
||||
7400,
|
||||
2200,
|
||||
value as usize,
|
||||
10000,
|
||||
);
|
||||
};
|
||||
let mut caches = crate::scene::build_derived_caches_with_progress(&doc, &cache_progress);
|
||||
caches.timings = crate::scene::OpenTimings {
|
||||
parse_ms,
|
||||
purge_ms,
|
||||
caches_ms: t_caches.elapsed().as_millis() as u32,
|
||||
xref_ms,
|
||||
};
|
||||
caches.corrupt_dropped = dropped;
|
||||
phase2.store(PHASE_FINALIZING, Ordering::Relaxed);
|
||||
caches.xref_dropped = xref_dropped;
|
||||
caches.xrefs = xref_infos;
|
||||
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9600, 0, 1);
|
||||
let (prepared_doc, prepared_geometry) =
|
||||
crate::scene::prepare_open_geometry(doc, &caches, model_bg);
|
||||
doc = prepared_doc;
|
||||
caches.prepared_geometry = Some(prepared_geometry);
|
||||
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9950, 1, 1);
|
||||
Ok((doc, caches))
|
||||
})();
|
||||
let _ = sender.send(result);
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| "parser thread panicked".to_string())??;
|
||||
.map_err(|error| format!("failed to start parser thread: {error}"))?;
|
||||
let (doc, caches) = receiver
|
||||
.await
|
||||
.map_err(|_| "parser thread stopped without a result".to_string())??;
|
||||
Ok((name, path, doc, caches))
|
||||
}
|
||||
|
||||
@@ -102,6 +206,7 @@ pub async fn open_path_with_phase(
|
||||
/// stands in for the document path.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_and_load_web(
|
||||
progress: Arc<OpenProgressState>,
|
||||
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title("Open CAD file")
|
||||
@@ -111,11 +216,23 @@ pub async fn pick_and_load_web(
|
||||
.await
|
||||
.ok_or_else(|| "Cancelled".to_string())?;
|
||||
let name = handle.file_name();
|
||||
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
||||
let bytes = handle.read().await;
|
||||
let mut doc = load_bytes(&name, bytes)?;
|
||||
progress.set(crate::app::OPEN_PHASE_PARSING, 1000, 0, 1);
|
||||
let mut doc = match web_worker::parse_document(&name, bytes).await {
|
||||
Ok(document) => document,
|
||||
Err(error) => return Err(format!("Web parser worker: {error}")),
|
||||
};
|
||||
if name.to_ascii_lowercase().ends_with(".dxf") {
|
||||
fix_dxf_dimension_rotations(&mut doc);
|
||||
}
|
||||
fix_viewport_status_flags(&mut doc);
|
||||
fix_current_style_names(&mut doc);
|
||||
progress.set(crate::app::OPEN_PHASE_CACHING, 7000, 0, 1);
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
let mut caches = crate::scene::build_derived_caches(&doc);
|
||||
caches.corrupt_dropped = dropped;
|
||||
progress.set(crate::app::OPEN_PHASE_FINALIZING, 9900, 1, 1);
|
||||
let path = PathBuf::from(&name);
|
||||
Ok((name, path, doc, caches))
|
||||
}
|
||||
@@ -168,6 +285,13 @@ fn sniff_dwg_or_dxf(path: &Path) -> String {
|
||||
}
|
||||
|
||||
pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
||||
load_file_with_progress(path, None)
|
||||
}
|
||||
|
||||
pub(crate) fn load_file_with_progress(
|
||||
path: &Path,
|
||||
_progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
||||
) -> Result<CadDocument, String> {
|
||||
let ext = path
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
@@ -184,10 +308,15 @@ pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
||||
match effective.as_str() {
|
||||
"dwg" => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut doc = DwgReader::from_mmap(path)
|
||||
.map_err(|e| e.to_string())?
|
||||
.read()
|
||||
let mut doc = {
|
||||
let mut reader = DwgReader::from_mmap(path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if let Some(progress) = _progress {
|
||||
reader.set_progress_callback(progress);
|
||||
}
|
||||
reader.read()
|
||||
.map_err(|e| e.to_string())?
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut doc = DwgReader::from_file(path)
|
||||
.map_err(|e| e.to_string())?
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use acadrust::CadDocument;
|
||||
use js_sys::{Array, Object, Reflect, Uint8Array};
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{ErrorEvent, MessageEvent, Worker, WorkerOptions, WorkerType};
|
||||
|
||||
pub(super) async fn parse_document(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
|
||||
let options = WorkerOptions::new();
|
||||
options.set_type(WorkerType::Module);
|
||||
let worker = Worker::new_with_options("ocs-parse-worker.js", &options).map_err(js_error)?;
|
||||
|
||||
let (sender, receiver) = iced::futures::channel::oneshot::channel();
|
||||
let sender = Rc::new(RefCell::new(Some(sender)));
|
||||
let message_sender = sender.clone();
|
||||
let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||
let data = event.data();
|
||||
let ok = Reflect::get(&data, &JsValue::from_str("ok"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
let result = if ok {
|
||||
Reflect::get(&data, &JsValue::from_str("data"))
|
||||
.map_err(js_error)
|
||||
.and_then(|value| {
|
||||
let bytes = Uint8Array::new(&value).to_vec();
|
||||
bincode::deserialize(&bytes).map_err(|error| error.to_string())
|
||||
})
|
||||
} else {
|
||||
Err(Reflect::get(&data, &JsValue::from_str("error"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_string())
|
||||
.unwrap_or_else(|| "CAD parser worker failed".to_string()))
|
||||
};
|
||||
if let Some(sender) = message_sender.borrow_mut().take() {
|
||||
let _ = sender.send(result);
|
||||
}
|
||||
});
|
||||
worker.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
|
||||
|
||||
let error_sender = sender;
|
||||
let on_error = Closure::<dyn FnMut(ErrorEvent)>::new(move |event: ErrorEvent| {
|
||||
if let Some(sender) = error_sender.borrow_mut().take() {
|
||||
let _ = sender.send(Err(event.message()));
|
||||
}
|
||||
});
|
||||
worker.set_onerror(Some(on_error.as_ref().unchecked_ref()));
|
||||
|
||||
let payload = Object::new();
|
||||
Reflect::set(
|
||||
&payload,
|
||||
&JsValue::from_str("name"),
|
||||
&JsValue::from_str(name),
|
||||
)
|
||||
.map_err(js_error)?;
|
||||
let input = Uint8Array::from(bytes.as_slice());
|
||||
Reflect::set(&payload, &JsValue::from_str("bytes"), &input.buffer()).map_err(js_error)?;
|
||||
let transfer = Array::new();
|
||||
transfer.push(&input.buffer());
|
||||
worker
|
||||
.post_message_with_transfer(&payload, &transfer)
|
||||
.map_err(js_error)?;
|
||||
|
||||
let result = receiver
|
||||
.await
|
||||
.map_err(|_| "CAD parser worker closed without a result".to_string())?;
|
||||
worker.terminate();
|
||||
result
|
||||
}
|
||||
|
||||
fn js_error(value: JsValue) -> String {
|
||||
value
|
||||
.as_string()
|
||||
.unwrap_or_else(|| format!("browser worker error: {value:?}"))
|
||||
}
|
||||
+59
-3
@@ -41,6 +41,19 @@ pub struct XrefInfo {
|
||||
/// just like the host doc, so it gets the same corrupt-entity guard. Folding
|
||||
/// it in here avoids a second full-document `entities()` walk after resolve.
|
||||
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>, usize) {
|
||||
resolve_xrefs_with_progress(doc, base_dir, None)
|
||||
}
|
||||
|
||||
/// Resolve XREFs while reporting completed work units.
|
||||
///
|
||||
/// Each reference contributes 1000 parse units and 1000 merge units. Parsing
|
||||
/// progress comes directly from the DWG reader when available, so one large
|
||||
/// XREF advances smoothly instead of making the file-open bar appear frozen.
|
||||
pub fn resolve_xrefs_with_progress(
|
||||
doc: &mut CadDocument,
|
||||
base_dir: &Path,
|
||||
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
|
||||
) -> (Vec<XrefInfo>, usize) {
|
||||
// Auto-resolve every xref — frustum + LOD culling keep GPU cost bounded.
|
||||
let xref_entries: Vec<(String, String, Handle)> = doc
|
||||
.block_records
|
||||
@@ -48,6 +61,11 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
|
||||
.filter(|br| (br.flags.is_xref || br.flags.is_xref_overlay) && !br.xref_path.is_empty())
|
||||
.map(|br| (br.name.clone(), br.xref_path.clone(), br.handle))
|
||||
.collect();
|
||||
let xref_count = xref_entries.len();
|
||||
let total_units = xref_count.saturating_mul(2000);
|
||||
if let Some(progress) = &progress {
|
||||
progress(0, total_units);
|
||||
}
|
||||
|
||||
// Phase 1 — parse every referenced file in parallel. Each `load_file`
|
||||
// reads and decodes an independent DWG/DXF and touches nothing in the host
|
||||
@@ -55,11 +73,40 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
|
||||
// instead of running back-to-back. (The merge in phase 2 mutates `doc`, so
|
||||
// it stays serial.) `resolve_path` is pure and `base_dir` is shared &-ref.
|
||||
use crate::par::prelude::*;
|
||||
let parse_units: std::sync::Arc<Vec<std::sync::atomic::AtomicU16>> = std::sync::Arc::new(
|
||||
(0..xref_count)
|
||||
.map(|_| std::sync::atomic::AtomicU16::new(0))
|
||||
.collect(),
|
||||
);
|
||||
let parsed: Vec<(String, String, Handle, Option<PathBuf>, Option<CadDocument>)> = xref_entries
|
||||
.into_par_iter()
|
||||
.map(|(block_name, raw_path, br_handle)| {
|
||||
.enumerate()
|
||||
.map(|(xref_index, (block_name, raw_path, br_handle))| {
|
||||
let resolved = resolve_path(&raw_path, base_dir);
|
||||
let xref_doc = resolved.as_ref().and_then(|p| super::load_file(p).ok());
|
||||
let units = std::sync::Arc::clone(&parse_units);
|
||||
let nested_progress = progress.as_ref().map(|progress| {
|
||||
let progress = std::sync::Arc::clone(progress);
|
||||
let callback: std::sync::Arc<dyn Fn(u16) + Send + Sync> =
|
||||
std::sync::Arc::new(move |value| {
|
||||
units[xref_index]
|
||||
.store(value.min(1000), std::sync::atomic::Ordering::Relaxed);
|
||||
let completed = units
|
||||
.iter()
|
||||
.map(|unit| unit.load(std::sync::atomic::Ordering::Relaxed) as usize)
|
||||
.sum();
|
||||
progress(completed, total_units);
|
||||
});
|
||||
callback
|
||||
});
|
||||
let xref_doc = resolved.as_ref().and_then(|p| super::load_file_with_progress(p, nested_progress).ok());
|
||||
parse_units[xref_index].store(1000, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(progress) = &progress {
|
||||
let completed = parse_units
|
||||
.iter()
|
||||
.map(|unit| unit.load(std::sync::atomic::Ordering::Relaxed) as usize)
|
||||
.sum();
|
||||
progress(completed, total_units);
|
||||
}
|
||||
(block_name, raw_path, br_handle, resolved, xref_doc)
|
||||
})
|
||||
.collect();
|
||||
@@ -68,7 +115,8 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
|
||||
// block order (par_iter preserves it), so handle allocation is deterministic.
|
||||
let mut result = Vec::with_capacity(parsed.len());
|
||||
let mut dropped = 0usize;
|
||||
for (block_name, raw_path, br_handle, resolved, xref_doc) in parsed {
|
||||
for (merge_index, (block_name, raw_path, br_handle, resolved, xref_doc)) in parsed.into_iter().enumerate()
|
||||
{
|
||||
let status = if let Some(xref_doc) = xref_doc {
|
||||
ensure_block_entities(doc, &block_name);
|
||||
dropped += merge_xref_into_block(doc, &block_name, br_handle, xref_doc);
|
||||
@@ -85,6 +133,14 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
|
||||
.unwrap_or(raw_path),
|
||||
status,
|
||||
});
|
||||
if let Some(progress) = &progress {
|
||||
progress(
|
||||
xref_count
|
||||
.saturating_mul(1000)
|
||||
.saturating_add((merge_index + 1).saturating_mul(1000)),
|
||||
total_units,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(result, dropped)
|
||||
|
||||
Reference in new issue
Block a user