Skip to content

Commit

Permalink
feat: detect compilation-related vfs changes (#1199)
Browse files Browse the repository at this point in the history
  • Loading branch information
Myriad-Dreamin authored Jan 19, 2025
1 parent e4bf2e9 commit d5ecf05
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 70 deletions.
25 changes: 24 additions & 1 deletion crates/tinymist-project/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -99,6 +99,7 @@ impl<F: CompilerFeat + 'static> CompileSnapshot<F> {
snap,
doc,
warnings,
deps: OnceLock::default(),
}
}
}
Expand All @@ -122,6 +123,8 @@ pub struct CompiledArtifact<F: CompilerFeat> {
pub warnings: EcoVec<SourceDiagnostic>,
/// The compiled document.
pub doc: SourceResult<Arc<TypstDocument>>,
/// The depended files.
pub deps: OnceLock<EcoVec<FileId>>,
}

impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
Expand All @@ -138,6 +141,7 @@ impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
snap: self.snap.clone(),
doc: self.doc.clone(),
warnings: self.warnings.clone(),
deps: self.deps.clone(),
}
}
}
Expand All @@ -150,6 +154,17 @@ impl<F: CompilerFeat> CompiledArtifact<F> {
.cloned()
.or_else(|| self.snap.success_doc.clone())
}

pub fn depended_files(&self) -> &EcoVec<FileId> {
self.deps.get_or_init(|| {
let mut deps = EcoVec::default();
self.world.iter_dependencies(&mut |f| {
deps.push(f);
});

deps
})
}
}

pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 32 additions & 8 deletions crates/tinymist-vfs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,11 @@ pub struct Vfs<M: PathAccessModel + Sized> {

impl<M: PathAccessModel + Sized> fmt::Debug for Vfs<M> {
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()
}
}

Expand All @@ -211,10 +215,25 @@ impl<M: PathAccessModel + Clone + Sized> Vfs<M> {
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<M: PathAccessModel + Sized> Vfs<M> {
Expand Down Expand Up @@ -247,7 +266,7 @@ impl<M: PathAccessModel + Sized> Vfs<M> {
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,
}
}
Expand Down Expand Up @@ -326,17 +345,20 @@ impl<M: PathAccessModel + Sized> Vfs<M> {

/// Reads a file.
pub fn read(&self, fid: TypstFileId) -> FileResult<Bytes> {
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<Source> {
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
Expand Down Expand Up @@ -442,6 +464,7 @@ impl<M: PathAccessModel + Sized> 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();
});
Expand Down Expand Up @@ -524,6 +547,7 @@ type BytesQuery = Arc<OnceLock<(Option<ImmutPath>, usize, FileResult<Bytes>)>>;

#[derive(Debug, Clone, Default)]
struct VfsEntry {
changed_at: usize,
bytes: BytesQuery,
source: Arc<OnceLock<FileResult<Source>>>,
}
Expand Down
60 changes: 7 additions & 53 deletions crates/tinymist-world/src/source.rs
Original file line number Diff line number Diff line change
@@ -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<S, E> = QueryRef<S, E, Option<S>>;
use typst::diag::{FileError, FileResult};
use typst::syntax::Source;

type FileQuery<T> = QueryRef<T, FileError>;
type IncrFileQuery<T> = IncrQueryRef<T, FileError>;

pub trait Revised {
fn last_accessed_rev(&self) -> NonZeroUsize;
}

pub struct SourceCache {
touched_by_compile: bool,
fid: TypstFileId,
source: IncrFileQuery<Source>,
source: FileQuery<Source>,
buffer: FileQuery<Bytes>,
}

Expand Down Expand Up @@ -103,22 +92,7 @@ impl SourceDb {
/// See `Vfs::resolve_with_f` for more information.
pub fn source(&self, fid: TypstFileId, p: &impl FsProvider) -> FileResult<Source> {
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()
})
}

Expand All @@ -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 {
Expand All @@ -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
})?)
}
17 changes: 15 additions & 2 deletions crates/tinymist-world/src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ impl<F: CompilerFeat> CompilerUniverse<F> {
pub fn increment_revision<T>(&mut self, f: impl FnOnce(&mut RevisingUniverse<F>) -> 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,
Expand Down Expand Up @@ -265,7 +267,9 @@ impl<F: CompilerFeat> EntryManager for CompilerUniverse<F> {
pub struct RevisingUniverse<'a, F: CompilerFeat> {
view_changed: bool,
vfs_revision: NonZeroUsize,
font_changed: bool,
font_revision: Option<NonZeroUsize>,
registry_changed: bool,
registry_revision: Option<NonZeroUsize>,
pub inner: &'a mut CompilerUniverse<F>,
}
Expand Down Expand Up @@ -298,6 +302,7 @@ impl<F: CompilerFeat> 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();
Expand All @@ -316,10 +321,12 @@ impl<F: CompilerFeat> RevisingUniverse<'_, F> {
}

pub fn set_fonts(&mut self, fonts: Arc<F::FontResolver>) {
self.font_changed = true;
self.inner.font_resolver = fonts;
}

pub fn set_package(&mut self, packages: Arc<F::Registry>) {
self.registry_changed = true;
self.inner.registry = packages;
}

Expand All @@ -340,6 +347,7 @@ impl<F: CompilerFeat> 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();
}

Expand All @@ -351,11 +359,12 @@ impl<F: CompilerFeat> 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 {
Expand Down Expand Up @@ -447,6 +456,10 @@ impl<F: CompilerFeat> CompilerWorld<F> {
self.source_db.take_state()
}

pub fn vfs(&self) -> &Vfs<F::AccessModel> {
&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.
Expand Down
35 changes: 32 additions & 3 deletions crates/tinymist/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl LspPreviewState {
#[derive(Default)]
pub struct ProjectStateExt {
pub is_compiling: bool,
pub last_compilation: Option<CompiledArtifact<LspCompilerFeat>>,
}

/// LSP project compiler.
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -228,6 +230,33 @@ impl CompileHandler<LspCompilerFeat, ProjectStateExt> 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;
};
Expand Down Expand Up @@ -287,7 +316,7 @@ impl CompileHandler<LspCompilerFeat, ProjectStateExt> 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;
Expand Down Expand Up @@ -407,11 +436,11 @@ impl QuerySnap {
pub fn run_analysis<T>(self, f: impl FnOnce(&mut LocalContextGuard) -> T) -> anyhow::Result<T> {
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}")
})?;

Expand Down
Loading

0 comments on commit d5ecf05

Please sign in to comment.