From d5ecf052d483ba11301c4733de5bb4ff61e3e6c3 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Mon, 20 Jan 2025 01:38:40 +0800 Subject: [PATCH] feat: detect compilation-related vfs changes (#1199) --- crates/tinymist-project/src/compiler.rs | 25 ++++++++++- crates/tinymist-vfs/src/lib.rs | 40 +++++++++++++---- crates/tinymist-world/src/source.rs | 60 +++---------------------- crates/tinymist-world/src/world.rs | 17 ++++++- crates/tinymist/src/project.rs | 35 +++++++++++++-- crates/tinymist/src/server.rs | 6 +-- 6 files changed, 113 insertions(+), 70 deletions(-) diff --git a/crates/tinymist-project/src/compiler.rs b/crates/tinymist-project/src/compiler.rs index 8ce995734..2e0678055 100644 --- a/crates/tinymist-project/src/compiler.rs +++ b/crates/tinymist-project/src/compiler.rs @@ -26,7 +26,7 @@ use crate::LspCompilerFeat; use tinymist_world::{ vfs::{ notify::{FilesystemEvent, MemoryEvent, NotifyMessage, UpstreamUpdateEvent}, - FsProvider, RevisingVfs, + FileId, FsProvider, RevisingVfs, }, CompilerFeat, CompilerUniverse, CompilerWorld, EntryReader, EntryState, TaskInputs, WorldDeps, }; @@ -99,6 +99,7 @@ impl CompileSnapshot { snap, doc, warnings, + deps: OnceLock::default(), } } } @@ -122,6 +123,8 @@ pub struct CompiledArtifact { pub warnings: EcoVec, /// The compiled document. pub doc: SourceResult>, + /// The depended files. + pub deps: OnceLock>, } impl std::ops::Deref for CompiledArtifact { @@ -138,6 +141,7 @@ impl Clone for CompiledArtifact { snap: self.snap.clone(), doc: self.doc.clone(), warnings: self.warnings.clone(), + deps: self.deps.clone(), } } } @@ -150,6 +154,17 @@ impl CompiledArtifact { .cloned() .or_else(|| self.snap.success_doc.clone()) } + + pub fn depended_files(&self) -> &EcoVec { + self.deps.get_or_init(|| { + let mut deps = EcoVec::default(); + self.world.iter_dependencies(&mut |f| { + deps.push(f); + }); + + deps + }) + } } pub trait CompileHandler: Send + Sync + 'static { @@ -225,6 +240,14 @@ impl CompileReasons { pub fn any(&self) -> bool { self.by_memory_events || self.by_fs_events || self.by_entry_update } + + pub fn exclude(&self, excluded: Self) -> Self { + Self { + by_memory_events: self.by_memory_events && !excluded.by_memory_events, + by_fs_events: self.by_fs_events && !excluded.by_fs_events, + by_entry_update: self.by_entry_update && !excluded.by_entry_update, + } + } } fn no_reason() -> CompileReasons { diff --git a/crates/tinymist-vfs/src/lib.rs b/crates/tinymist-vfs/src/lib.rs index 38a3c4278..9ee5f2f92 100644 --- a/crates/tinymist-vfs/src/lib.rs +++ b/crates/tinymist-vfs/src/lib.rs @@ -187,7 +187,11 @@ pub struct Vfs { impl fmt::Debug for Vfs { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Vfs").finish() + f.debug_struct("Vfs") + .field("revision", &self.revision) + .field("managed", &self.managed.lock().entries.size()) + .field("paths", &self.paths.lock().paths.len()) + .finish() } } @@ -211,10 +215,25 @@ impl Vfs { source_cache: self.source_cache.clone(), managed: Arc::new(Mutex::new(EntryMap::default())), paths: Arc::new(Mutex::new(PathMap::default())), - revision: NonZeroUsize::new(1).expect("initial revision is 1"), + revision: NonZeroUsize::new(2).expect("initial revision is 2"), access_model: self.access_model.clone(), } } + + pub fn is_clean_compile(&self, rev: usize, file_ids: &[FileId]) -> bool { + let mut m = self.managed.lock(); + for id in file_ids { + let Some(entry) = m.entries.get_mut(id) else { + log::info!("Vfs(dirty, {id:?}): file id not found"); + return false; + }; + if entry.changed_at == 0 || entry.changed_at >= rev { + log::info!("Vfs(dirty, {id:?}): rev {rev:?} => {:?}", entry.changed_at); + return false; + } + } + true + } } impl Vfs { @@ -247,7 +266,7 @@ impl Vfs { source_cache: SourceCache::default(), managed: Arc::default(), paths: Arc::default(), - revision: NonZeroUsize::new(1).expect("initial revision is 1"), + revision: NonZeroUsize::new(2).expect("initial revision is 2"), access_model, } } @@ -326,17 +345,20 @@ impl Vfs { /// Reads a file. pub fn read(&self, fid: TypstFileId) -> FileResult { - let bytes = self.managed.lock().slot(fid, |entry| entry.bytes.clone()); + let bytes = self.managed.lock().slot(fid, |entry| { + entry.changed_at = entry.changed_at.max(1); + entry.bytes.clone() + }); self.read_content(&bytes, fid).clone() } /// Reads a source. pub fn source(&self, file_id: TypstFileId) -> FileResult { - let (bytes, source) = self - .managed - .lock() - .slot(file_id, |entry| (entry.bytes.clone(), entry.source.clone())); + let (bytes, source) = self.managed.lock().slot(file_id, |entry| { + entry.changed_at = entry.changed_at.max(1); + (entry.bytes.clone(), entry.source.clone()) + }); let source = source.get_or_init(|| { let content = self @@ -442,6 +464,7 @@ impl RevisingVfs<'_, M> { fn invalidate_file_id(&mut self, file_id: TypstFileId) { self.view_changed = true; self.managed.slot(file_id, |e| { + e.changed_at = self.inner.revision.get(); e.bytes = Arc::default(); e.source = Arc::default(); }); @@ -524,6 +547,7 @@ type BytesQuery = Arc, usize, FileResult)>>; #[derive(Debug, Clone, Default)] struct VfsEntry { + changed_at: usize, bytes: BytesQuery, source: Arc>>, } diff --git a/crates/tinymist-world/src/source.rs b/crates/tinymist-world/src/source.rs index 4825c929b..192938be5 100644 --- a/crates/tinymist-world/src/source.rs +++ b/crates/tinymist-world/src/source.rs @@ -1,31 +1,20 @@ // use std::sync::Arc; use core::fmt; -use std::{num::NonZeroUsize, sync::Arc}; +use std::sync::Arc; use parking_lot::Mutex; -use tinymist_std::hash::FxHashMap; -use tinymist_std::QueryRef; +use tinymist_std::{hash::FxHashMap, QueryRef}; use tinymist_vfs::{Bytes, FsProvider, TypstFileId}; -use typst::{ - diag::{FileError, FileResult}, - syntax::Source, -}; - -/// incrementally query a value from a self holding state -type IncrQueryRef = QueryRef>; +use typst::diag::{FileError, FileResult}; +use typst::syntax::Source; type FileQuery = QueryRef; -type IncrFileQuery = IncrQueryRef; - -pub trait Revised { - fn last_accessed_rev(&self) -> NonZeroUsize; -} pub struct SourceCache { touched_by_compile: bool, fid: TypstFileId, - source: IncrFileQuery, + source: FileQuery, buffer: FileQuery, } @@ -103,22 +92,7 @@ impl SourceDb { /// See `Vfs::resolve_with_f` for more information. pub fn source(&self, fid: TypstFileId, p: &impl FsProvider) -> FileResult { self.slot(fid, |slot| { - slot.source - .compute_with_context(|prev| { - let content = p.read(fid)?; - let next = from_utf8_or_bom(&content)?.to_owned(); - - // otherwise reparse the source - match prev { - Some(mut source) => { - source.replace(&next); - Ok(source) - } - // Return a new source if we don't have a reparse feature or no prev - _ => Ok(Source::new(fid, next)), - } - }) - .cloned() + slot.source.compute(|| p.read_source(fid)).cloned() }) } @@ -129,7 +103,7 @@ impl SourceDb { let entry = slots.entry(fid).or_insert_with(|| SourceCache { touched_by_compile: self.is_compiling, fid, - source: IncrFileQuery::with_context(None), + source: FileQuery::default(), buffer: FileQuery::default(), }); if self.is_compiling && !entry.touched_by_compile { @@ -150,23 +124,3 @@ impl SourceDb { } } } - -pub trait MergeCache: Sized { - fn merge(self, _other: Self) -> Self { - self - } -} - -pub struct FontDb {} -pub struct PackageDb {} - -/// Convert a byte slice to a string, removing UTF-8 BOM if present. -fn from_utf8_or_bom(buf: &[u8]) -> FileResult<&str> { - Ok(std::str::from_utf8(if buf.starts_with(b"\xef\xbb\xbf") { - // remove UTF-8 BOM - &buf[3..] - } else { - // Assume UTF-8 - buf - })?) -} diff --git a/crates/tinymist-world/src/world.rs b/crates/tinymist-world/src/world.rs index 75129bf2d..868b0a707 100644 --- a/crates/tinymist-world/src/world.rs +++ b/crates/tinymist-world/src/world.rs @@ -121,7 +121,9 @@ impl CompilerUniverse { pub fn increment_revision(&mut self, f: impl FnOnce(&mut RevisingUniverse) -> T) -> T { f(&mut RevisingUniverse { vfs_revision: self.vfs.revision(), + font_changed: false, font_revision: self.font_resolver.revision(), + registry_changed: false, registry_revision: self.registry.revision(), view_changed: false, inner: self, @@ -265,7 +267,9 @@ impl EntryManager for CompilerUniverse { pub struct RevisingUniverse<'a, F: CompilerFeat> { view_changed: bool, vfs_revision: NonZeroUsize, + font_changed: bool, font_revision: Option, + registry_changed: bool, registry_revision: Option, pub inner: &'a mut CompilerUniverse, } @@ -298,6 +302,7 @@ impl Drop for RevisingUniverse<'_, F> { view_changed = true; // The registry has changed affects the vfs cache. + log::info!("resetting shadow registry_changed"); self.vfs().reset_cache(); } let view_changed = view_changed || self.vfs_changed(); @@ -316,10 +321,12 @@ impl RevisingUniverse<'_, F> { } pub fn set_fonts(&mut self, fonts: Arc) { + self.font_changed = true; self.inner.font_resolver = fonts; } pub fn set_package(&mut self, packages: Arc) { + self.registry_changed = true; self.inner.registry = packages; } @@ -340,6 +347,7 @@ impl RevisingUniverse<'_, F> { // Resets the cache if the workspace root has changed. let root_changed = self.inner.entry.workspace_root() == state.workspace_root(); if root_changed { + log::info!("resetting shadow root_changed"); self.vfs().reset_cache(); } @@ -351,11 +359,12 @@ impl RevisingUniverse<'_, F> { } pub fn font_changed(&self) -> bool { - is_revision_changed(self.font_revision, self.font_resolver.revision()) + self.font_changed && is_revision_changed(self.font_revision, self.font_resolver.revision()) } pub fn registry_changed(&self) -> bool { - is_revision_changed(self.registry_revision, self.registry.revision()) + self.registry_changed + && is_revision_changed(self.registry_revision, self.registry.revision()) } pub fn vfs_changed(&self) -> bool { @@ -447,6 +456,10 @@ impl CompilerWorld { self.source_db.take_state() } + pub fn vfs(&self) -> &Vfs { + &self.vfs + } + /// Sets flag to indicate whether the compiler is currently compiling. /// Note: Since `CompilerWorld` can be cloned, you can clone the world and /// set the flag then to avoid affecting the original world. diff --git a/crates/tinymist/src/project.rs b/crates/tinymist/src/project.rs index 5cd9a0dfc..c6f9ab148 100644 --- a/crates/tinymist/src/project.rs +++ b/crates/tinymist/src/project.rs @@ -83,6 +83,7 @@ impl LspPreviewState { #[derive(Default)] pub struct ProjectStateExt { pub is_compiling: bool, + pub last_compilation: Option>, } /// LSP project compiler. @@ -129,6 +130,7 @@ impl Project { let proj = self.state.projects().find(|p| p.id == compiled.id); if let Some(proj) = proj { proj.ext.is_compiling = false; + proj.ext.last_compilation = Some(compiled.clone()); } } @@ -228,6 +230,33 @@ impl CompileHandler for CompileHandlerImpl { continue; } + let reason = s.reason; + + const VFS_SUB: CompileReasons = CompileReasons { + by_memory_events: true, + by_fs_events: true, + by_entry_update: false, + }; + + let is_vfs_sub = reason.any() && !reason.exclude(VFS_SUB).any(); + let id = &s.id; + + if is_vfs_sub + && 'vfs_is_clean: { + let Some(compilation) = &s.ext.last_compilation else { + break 'vfs_is_clean false; + }; + + let last_rev = compilation.world.vfs().revision(); + let deps = compilation.depended_files().clone(); + s.verse.vfs().is_clean_compile(last_rev.get(), &deps) + } + { + log::info!("Project: skip compilation for {id:?} due to harmless vfs changes"); + s.reason = CompileReasons::default(); + continue; + } + let Some(compile_fn) = s.may_compile(&c.handler) else { continue; }; @@ -287,7 +316,7 @@ impl CompileHandler for CompileHandlerImpl { let mut n_rev = self.notified_revision.lock(); if *n_rev >= snap.world.revision().get() { log::info!( - "TypstActor: already notified for revision {} <= {n_rev}", + "Project: already notified for revision {} <= {n_rev}", snap.world.revision(), ); return; @@ -407,11 +436,11 @@ impl QuerySnap { pub fn run_analysis(self, f: impl FnOnce(&mut LocalContextGuard) -> T) -> anyhow::Result { let world = self.snap.world; let Some(main) = world.main_id() else { - error!("TypstActor: main file is not set"); + error!("Project: main file is not set"); bail!("main file is not set"); }; world.source(main).map_err(|err| { - info!("TypstActor: failed to prepare main file: {err:?}"); + info!("Project: failed to prepare main file: {err:?}"); anyhow::anyhow!("failed to get source: {err}") })?; diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index 9cd8f382e..d5dbc70c9 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -286,15 +286,15 @@ impl LanguageState { mut state: ServiceState, params: LspInterrupt, ) -> anyhow::Result<()> { - let start = std::time::Instant::now(); - log::info!("incoming interrupt: {params:?}"); + let _start = std::time::Instant::now(); + // log::info!("incoming interrupt: {params:?}"); let Some(ready) = state.ready() else { log::info!("interrupted on not ready server"); return Ok(()); }; ready.project.interrupt(params); - log::info!("interrupted in {:?}", start.elapsed()); + // log::info!("interrupted in {:?}", _start.elapsed()); Ok(()) } }