Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better VFS #3715

Closed
matklad opened this issue Mar 25, 2020 · 27 comments
Closed

Better VFS #3715

matklad opened this issue Mar 25, 2020 · 27 comments
Labels
C-Architecture Big architectural things which we need to figure up-front (or suggestions for rewrites :0) )

Comments

@matklad
Copy link
Member

matklad commented Mar 25, 2020

Our VFS has a number of implementation and design problem. Being the core of the system, this is not good :-)

The main impl problem is that we hit many bugs in the underlying notify library (so that we try to use client-side watching by default) (and the root cause here is that the OS APIs for file watching are just terrible and nay impossible to use in a non-broken way).

Some of design issues are:

  • conflict between eager loading and lazy loading of files. To have consistent snapshots of FS, we must crawl the fs and now the set of files up front. But user can also ask us about files outside of any particular watched directory, and ideally we shouldn't spam "file is outside of the watched cargo workspace" errors.
  • conflict between the desire to have roots (and classify each file as belonging to exactly one root) and the desire to change the set of roots dynamically.
  • there should be a way to reload the VFS (which should keep file ids stable)
  • there should be a way to restart the process, while keeping file ids stable

I hope that most of this design issues are actually handled by https://docs.rs/watchman_client/

@matklad matklad added the C-Architecture Big architectural things which we need to figure up-front (or suggestions for rewrites :0) ) label Mar 25, 2020
@edwin0cheng
Copy link
Member

Related #3594 #3414 #1968

@wez
Copy link

wez commented May 18, 2020

https://rust-analyzer.github.io/blog/2020/05/18/next-few-years.html

One specific question I have is: "How does watchman handle dynamic addition/removal of projects?". If you have any experience to share, please comment on the VFS issue in rust-analyzer.

Hello, I'm deeply involved in both watchman and large virtual filesystems at FB. Maybe it's worth us having a realtime chat sometime soonish? I'm interested in hearing more about what's on your mind and can share context on how we think about the problem space in general and perhaps give some advice to inform your design!

@matklad
Copy link
Member Author

matklad commented May 18, 2020

Yes, this would be hugely helpful! The best place to coordinate about specific for me would be the rust-lang Zulip instance https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0 (@matklad). Email (on GitHub profile) would also work!

@golddranks
Copy link

Just to shout out: (already did that in Zulip) I'm also interested in improving VFS! However, I know hardly anything about Watchman, but if there is some other grunt work to pave the way to real-time updating Cargo.toml/Cargo.lock, adding files/workspaces dynamically and opening single files without projects, I'm interested!

@stuhood
Copy link

stuhood commented May 18, 2020

Responding to the blog post (which was great, thanks!) with some prior art in this area: the Pants build system implements a lazy VFS with symlink aware file watching, spread across three-ish relatively loosely coupled crates. We recently switched from watchman to the notify crate in order to remove an external service (we're already a daemon), and minimize eager watching.


The fs crate implements symlink aware lazy glob expansion atop a VFS trait. Being symlink aware means that the VFS never traverses a symlink implicitly: it always requests it from the VFS (which allows the VFS to track all such operations). The core exposed operation is to record Snapshots (internally, a merkle tree) of PathGlobs (git-style globs).

The core VFS impl that we have is small, but not directly reusable: it implements the methods by delegating to implementations of the Node trait exposed by the graph crate. Before running a file-related Node, it also pokes the watch crate to ensure that we are watching the relevant path.

The graph crate is similar (in purpose at least?) to salsa: the idea is that you have a Node type (generally implemented using an enum to isolate the implementations), and as Nodes run they can request more dependencies, which are tracked in the Graph. Importantly, it exposes an invalidate_from_roots method which is called to invalidate/dirty the dependees of file operations when they change.

Finally, the watch crate is a thin wrapper around the notify crate, which is handed an instance of a one method Invalidatable trait: outside of tests, our invalidatable is always a Graph instance, which would like the transitive dependees of files invalidated.


If I understand salsa correctly (and rust-analyzer is still using it?), it's possible that you would be able to do things similar to the fs (implement capturing atop the VFS) and watch (generically poke something invalidatable) crates, and invalidate a graph managed by salsa.

@stuhood
Copy link

stuhood commented May 18, 2020

A bit more color on the notify/watchman choice, since that seems to be a key point here.

Pants used watchman successfully for a few years (thanks @wez!) but recently decided to switch away to avoid the need for a separate daemon with configuration around connection timeouts, a named socket to use, etc. Rust bindings for the core of watchman would be pretty cool...

The notify crate has experienced some bumps recently though: an ownership change, and bugs around debounced watching. Because we invalidate a graph that can tell whether a node has already been cleared, debouncing wasn't helpful for us.

Our hypothesis on this was that having a dependency aware, memoized graph and tracking operations at the syscall level made a few of the things that watchman does redundant. There are other things that we'll likely need to implement on our own though (re-crawl support, for example): so time will tell!

@golddranks
Copy link

golddranks commented May 19, 2020

So, trying to facilitate things a bit. Please correct me if I have anything incorrect or missing and I'll add it. Also marked places I'm unclear about, or know there's information missing, with FIXME:

What are the requirements for VFS to use with Rust-Analyzer?

  • Must provide consistent snapshots of the filesystem that doesn't change under your feet when doing analysis
  • The accessed data must be loaded in memory to confine IO to the VFS subsystem only
  • Must support file watching to be able to detect when to create new snapshots, to stay updated
  • Must be cross-platform (at least Win/Lin/Mac?)
  • Must provide platform abstractions: paths (platform paths → utf-8 paths), file contents (line endings → \n, FIXME: what about encodings, BOM?), symlinks (FIXME: what to do with them?)
  • Must support dynamically adding (FIXME: and removing?) files/dirs to keep track of

What are the shortcomings of current ra_vfs?

(@matklad's list is in the starting post, this is my rephrasing)

  • Dynamically adding/removing roots isn't supported
    • This is important in cases where...
      • the editor adds a workspace afterwards (LSP supports this)
      • the workspace isn't known upon start (with a single stand-alone file usecase)
        • I don't think LSP even necessitates having a "project root" at all.
      • the code tries to refer to files from outside the project (uncertain whether this should be actually supported as it isn't a good pattern)
        • this could happen inside of procedural macros (breaks determinism so not good)
        • or with #[path="foo.rs"] declaration on modules (again, referring outside the project seems to be bad)
        • FIXME: anything else?
  • Consistency not guaranteed with lazy loading? (FIXME: an example would be helpful, not sure if I understand the problem)
  • Keeping file IDs stable under reload/restart isn't supported (FIXME: what's the benefit? again, example would be great)

What are the alternatives / future possibilities?

Moving from notify crate to Watchman

  • What are the benefits?
    • Handles OS-level quirks
    • Battle tested
    • Scales for huge monorepos (virtual file system and VCS integration)
    • Scales for big tool stacks
  • What are the downsides?
    • A separate daemon?
      • Makes the editor-RA-watcher system doubly distributed system? That doesn't sound great?
    • Seems harder to distribute? (FIXME: is it? Do we require users to install Watchman? Is there a "watchman as a library"? Can it be rustup'd?)

Staying with notify?

  • What are the downsides? (FIXME: Do we need to fix bugs?)

Developing ra_vfs's other features that don't directly concern file watching

  • FIXME: Is the choice of the file watching implementation a blocking concern for improving other things?

I'd appreciate if @matklad and others who have good understanding of the problem would fill in the FIXMEs and point if there's other stuff missing.

Updates:
2020-05-19 10:19 UTC Added upsides of Watchman

@wez
Copy link

wez commented May 19, 2020

[Disclosure: I'm heavily invested in both Watchman and EdenFS at FB. I have two different "hats" here in this thread; one is to be a resource to help you understand more about Watchman, the other is as a Rust enthusiast that wants to see more Rust in our monorepo and to facilitate those tools working well in that environment!]

Watchman's selling points are mostly to do with scale of the repo, which very few (none that I'm aware of!) other notification libraries consider:

  • The separate watchman daemon process allows sharing the OS level watch resources across multiple tools (source control, build, IDEs, type checkers for multiple languages, linters, automatic test runners, fuzzy file finders, code indexers). This is important for very large projects (we're planning for many many millions of files and more) where it is cost-prohibitive (time, IO and memory) for each of them to build up their own model and watch resources. If you can get away with only monitoring a handful of files then this is less applicable, but for a very large repo with sources spread throughout this becomes more relevant. Because of these concerns: the monitoring portion of watchman is not available as an embeddable library, however, the server activation by the client library is seamless and doesn't require any privileged installation beyond ensuring that the OS inotify sysctls are set appropriately for the system.

  • watchman can (and has!) been taught about virtual filesystems, such as EdenFS, that lazily materialize file content in a respository. Native OS watchers cannot provide correct results across checkout and rebase operations without materializing the entire repo, which is extremely inefficient in this kind of filesystem and results in truly wretched user experience.

  • Handles a variety of OS level quirks in order to provide reliable results, or to inform you when reliable results are impossible. While it is easy to get notification streams from the OS, producing an accurate and reliable model from that is fraught with TOCTOU concerns.

  • Is proven/battle-tested/mature having been in service for >7 years in a large scale deployment serving both humans and CI infrastructure on Linux, macOS and Windows systems.

  • Has a query mode that is able to integrate with source control (currently hg only, but the concepts also apply to git) which helps to translate big rebases into deltas. For example, if a pull --rebase physically changes 50k files on disk but your local changes only modify 20 files prior to the pull, and you can pull a pre-computed index for a recent revision off master, then: you can use this information to load that index and then incrementally process only the 20 local files on top, rather than being forced to process 50k files. Watchman doesn't provide the index storage or retrieval but can be taught how to query storage systems for different kinds of index.

Today it is admittedly difficult to consume the version of watchman from master because it is difficult to build unless you are thoroughly determined, BUT! we've got the GitHub CI producing binaries and in the next few weeks should be able to iron out some kinks and get those showing up in the releases section. My plan for Linux is to ship an AppImage binary, and it should be doable to bundle the linux/windows/macos binaries up with rust-analyzer if that makes sense for your deployment.

Not everyone has the same scaling requirements as FB so the above isn't universally relevant. What matters to us is being able to configure the tools to use watchman, even if that isn't the default/only way to make the tool function. So while I'm pro-watchman I'm enough of a pragmatist not to campaign only for watchman vs. some other solution--"why not both?" seems like a valid question to ask.

@golddranks
Copy link

producing an accurate and reliable model from that is fraught with TOCTOU concerns.

This sounds interesting to me. Does Watchman not only provide update notifications but the changed file contents? It sounds to me that no matter how consistent the file metadata model is, if ra_vfs does the file loading by itself, it's bound to have TOCTOU concerns.

@bjorn3
Copy link
Member

bjorn3 commented May 19, 2020

My plan for Linux is to ship an AppImage binary

Watchman only needs libc, right? In that case using musl to statically link would be a much more lightweight solution. (Maybe with jemalloc for much better allocator performance?) There is no need to mount a file system that contains the executable and all files it depends on, like AppImage does. https://docs.appimage.org/user-guide/run-appimages.html#mount-an-appimage

@golddranks
Copy link

golddranks commented May 19, 2020

Another request to educate me (consider it rubber duck debugging if you will): what does consistency mean in the context of VFS and what level of consistency is required for the use case? The way I imagine things is that the creation, deletion and updated events of files should be known in a consistent order after they have added to watched roots. But when loading the contents after getting a notification of an update, we might get a version that is newer, because we haven't got the update for that change yet?

Is that a problem? Is there other, more serious consistency problems I'm missing? I have the impression that 1) consequent filesystem snapshots are still "monotonically increasing" 2) we don't get stuck in a state where we stay at an old version of a file for an indefinitely long time if don't miss any updates after we've first started watching a file and then loaded it.

I used to think that the "consistency" required is only that once you got some versions of the files loaded in memory, you can rely on that snapshot not changing, but that is provided just by having the files loaded to memory and being immutable and isn't dependent on the file watcher's functionality. What are the kind of consistency problems around file watching we care about?

@wez
Copy link

wez commented May 19, 2020

Regarding TOCTOU and consistency; here are some more implementation details from watchman. I'm using these to surface some of the concerns we've had to deal with in our role as a general file watching system. These concerns may not apply to every use case!

The OS notification stream is buffered by the kernel which means that you need a mechanism to relate an event with "when" it happened, because the time at which you pull the event from the stream is not the time at which it happened, making it potentially racy to understand whether you are up to date.

Watchman deals with this by generating a special unique cookie file at the start of a query and waiting to observe the notifications for that file before proceeding; more details here: https://facebook.github.io/watchman/docs/cookies

Another aspect of consistency is that because processing of events lags behind the notification stream, the nature of the notification may not reflect the currently observable state on the filesystem! Consider a program that creates a temporary file, writes to it and then unlinks it. Notifications are likely generated for at least three events and depending on how quickly those states transition, stat(2) the file may not match the information from the stream. (watchman also has a concept of settling to help de-bounce and improve sanity in these sorts of rapidly changing views)

Some notification systems are potentially low fidelity; for example, on macOS/fsevents, you can get an event that says "something somewhere under this directory path has changed", but it won't tell you what. It may mean that the whole directory subtree is different, or that some significant amount of it is different.

To make it easier to reason about the state of the filesystem, watchman uses the OS notification stream as a source of hints rather than trusted metadata. Watchman builds a model of the observed filesystem and uses those hints to detect when it should update its model. Each time the notification stream provides data, watchman assigns the next monotonic sequence number (called the clock) that will represent that point in time and then uses the hints to stat/readdir and update its model, tagging newly updated entries with the new clock and this is used to build a recency index (which is an intrusively doubly linked list through the file information, ordered by recency).

The clock allows watchman clients to query about changes since a prior clock.

Note that because of TOCTOU and the asynchronous nature of everything, watchman's observation based model only guarantees that changes prior to the creation of the synchronization cookie file have been observed. Effects from events that happened concurrently with or after that may also be included in that view, but are guaranteed to subsequently wake the notification stream and tick the clock, so your application can reason about that subsequent change.

A common problem to deal with in this sort of situation is a person who kicks off a build and who edits files while the build is running. In this racy scenario it is ambiguous whether their edits were observed by the compiler, and the compiler generally can't tell that this has happened because it is only concerned with the data it read from the file at the moment it considered it. At best the resultant artifact has ambiguous contents, but if you are caching eg: metadata about symbols/exports from a source file and plan to re-use it on a subsequent run, you may have a poisoned cache entry. One strategy to deal with this is:

  • Query watchman for the changes since the last build/indexing run. Record the clock the value that is included in those results--it represents the state at the start of build.
  • Process the file results and update state/caches/artifacts
  • Once complete, query watchman using the clock value from the start of the build. If any files are returned that might have influenced the build you can then choose to invalidate caches and warn the user (or just error out with a message explaining) that the build was inconsistent.

Regarding the snapshot concept:

I've seen snapshot mentioned a couple of times in this issue; I don't really know anything about the current design or what that means in the context of this project. However, I'd suggest being careful with this term because it implies a level of consistency that is very difficult to obtain without true snapshot support from the underlying filesystem! I'm not sure that you can obtain a stronger ordering/consistency than the happens-before/"eventually consistent" concept above.

Regarding file contents:

Watchman builds its model from the metadata and avoids looking at file contents. The model reflects the most recently observed state of the filesystem and doesn't include history, so while you can ask watchman to execute a glob, you can't ask it to execute a glob on a prior view of the filesystem.

Watchman can be asked to produce a content hash for a file; today it knows about sha1 hashes because those are widely used for change detection (not for crypto!) in tools widely used inside FB. Watchman maintains an LRU cache for content hashes and the size can be specified in a .watchmanconfig file stored in the repo root, and can be configured to eagerly compute hashes for a bounded number of recently changed files. When the underlying repo is EdenFS, the filesystem can potentially furnish content hashes from the underlying source control data without the need to download the entire file.

Aside from content hashes, watchman avoids operating on file contents because the IO and memory requirements are hard to predict; eg: someone fat-fingers a command and now you have a 100GB .rs that is really a log file that you're trying to read into memory on a system with 8GB of RAM.

@wez
Copy link

wez commented May 19, 2020

My plan for Linux is to ship an AppImage binary

Watchman only needs libc, right? In that case using musl to statically link would be a much more lightweight solution. (Maybe with jemalloc for much better allocator performance?) There is no need to mount a file system that contains the executable and all files it depends on, like AppImage does. https://docs.appimage.org/user-guide/run-appimages.html#mount-an-appimage

The folly and thrift (for talking to EdenFS) dependencies pull in boost, glog and gflags and for difficult to unpick reasons, the cmake flavor of the builds for these things are resistant to compiling their various libraries statically in all combinations of the open sourced FB projects. We've gotten some of them static, but it's hard and thankless work requiring the patience of a saint / the stubbornness of a mule.

AppImage is just an expedient way to furnish a binary download; it's not technically required and the cost/benefit of making a musl / all static build vs. the cost of getting there don't look compelling today. We'd like to see it happen, but it's not a small amount of work because it would need to apply across all of the facebook projects that depend on folly.

@golddranks
Copy link

golddranks commented May 19, 2020

@wez Thanks for the explanation. For clarification: I use snapshot here to mean an immutable in-memory state of the contents and path-content mappings of the files included in the VFS overlay; so not in a meaning of getting an atomic state of a filesystem but rather, making a best-effort try to load a recent state of the relevant files and then "freeze" that to use as input for the analysis libraries.

Thanks for the links, I think I'll take a better look at Watchman docs :)

@matklad
Copy link
Member Author

matklad commented May 20, 2020

Let me clarify my vision of "consistent snapshots" as well, as that's a pretty sloppy usage of terminology on my part.

The only property we really need from a VFS is repeatable read. If rust-analalyzer queries for a contents of file "foo.rs" and gets fn main() {} back, repeated queries for file text should return fn main() {} until rust-analyzer acknowledges an explicit "next revision" event.

We need this property, because rust-analyzer is architectured around ability to forget things. Our concrete syntax trees are a heavy data structure, so we generally parse file, compute some compressed index on the result, and throw away the syntax tree. If later we need syntax tree to get additional info about the file, rust-analyzer just re-parses it from scratch and, at this point, it is important that the re-parsed tree is consistent with the info we persisted in an index.

Obviously, file systems do not actually guarantee such repeatable read, so we have to keep the contents of files in memory. This should be OK even for large repos:

  • The size of files is pretty small in comparison to all other data structures used by r-a. One byte of input results in many bytes in memory. I don't know the exact value of "many", but I think it's somewhere between 10x-100x. That is, we run out of memory for everything else before we run out of memory for files.
  • If we really start running out of memory for storing files, we can always apply lightweight compression or store them in an on-disk database.

So "VFS" for rust-analyzer is (to the first approximation) a HashMap<PathBuf, Vec<u8>> plus some API to say "the new contents of this and that file is now this".

Curiously, rust-analyzer doesn't really care what it reads from the files, if that is eventually consistent. Ie, if mv foo.rs bar.rs produces a snapshot where both foo.rs and bar.rs exist, and the text of foo.rs magically changes to "здесь был Вася", that's totally OK, if we eventually get to a situation where only bar.rs exists, and with correct contents.

The only bad thing we need to protect against are lost updates. If, for example, rust-analyzer reads a directory, and then a file changes after we've read its contents but before we've set up a watch, we have a problem.

@kavirajk
Copy link

emacs user: currently goto-definition on external crate (added via cargo add ) doesn't work unless you do restart workspace. (lsp-restart-worspace)

would be interested to see that on rust-analyzer!

@golddranks
Copy link

golddranks commented May 26, 2020

We had a video discussion with @matklad and @wez around topics related to watching and modeling the file system. I'm summarizing some random thoughts and notes here.

Watching changes

Some hard things in this space (not necessarily an exhaustive list):

  • Rust explicitly requires mod annotations for including modules. Even if explicit, there are more than one ways how this maps to the filesystem. (foo.rs or foo/mod.rs, anything else?)
    • Rust-Analyzer wants to detect and be able to report conflicts such as having both. This means that in case one exists, and then user adds the other, Rust-Analyzer must be able to detect this file creation; it musn't be just "satisfied" with the file it already has.
  • Even more: if the user forgets to add a mod annotation, RA still wants to be aware of those "missing" module-like files that exist.
  • There is some value in getting file lists eagerly, to pre-compute stuff even for files that haven't been added to the project formally.
    • Was it also to avoid having to slowly crawl the tree via mod references? Getting a file list and starting parsing everything independently allows obviously for better parallelism.
  • There are cases where a Cargo.toml file is added to a parent directory where it didn't exist before. This obviously affects to how the project structure is interpreted, and it would be nice to be able to detect that.
  • There are cases where a build script generates Rust code that ends up in target directory. However, target might contain a huge amount of unrelated files and we'd generally want to ignore it.

Representing paths

  • In the end, there was some discussion about how to represent stuff, especially about file paths.

  • I'm not sure if we had anything conclusive, except that it's hard.

  • However @matklad pointed out that in Rust projects, the files are UTF-8 so obviously paths with too crazy encodings and such would be unrepresentable.

  • Here's a sampler of what I know about path encoding problems:

    • AFAIK, Windows (NTFS) paths are expected to be UTF-16, but it doesn't really check their correctness. This leads to weirdness such as:
      • Paths with single surrogate points.
      • Paths that are super mangled because some legacy program that doesn't know about Unicode wrote them and nothing checked upon them. (I have some experience of programs that expect everything to be ShiftJIS, a Japanese legacy-but-not-dead encoding.)
    • macOS paths (dunno about the new APFS thought) are traditionally case-insensitive. They also do some other stuff like convert stuff to NFD normalisation form which in my experience often breaks equality between paths containing some Japanese characters that look similar and are equivalent but do not compare codepoint-wise equal because they are in decomposed forms.
    • Unix paths are like "whatever byte string goes, except that b'/' is a separator and don't use b'\0'". In practice, almost all programs/scripts have more assumptions about "proper" paths and thus, tend to break with anything weird.
  • All in all, sounds super hard! But I think that we can go a long way without caring about non-Unicode paths because as @matklad said, those are not accessible from "inside the Rust universe" anyway. However, there are some things that aren't necessarily encoding problems and what we thus might have to care. Case-insensivity and NFD normalisation come to mind.

There was talk about the general requirements, about whether a set of APIs @matklad showed seemed to make sense, ideas about eagerness and laziness and stuff like that, but I didn't manage to get any conclusive picture about them.

There was also some thing I wanted to talk but it was getting late so I decided not to, but I will expand here: @matklad emphasized that he doesn't want to build and maintain a file watching / notification library himself. He also was interested in Watchman because it's battle-tested and they have been thinking hard about best practices of representing this stuff. However, I am a bit worried about the weight of the dependency in the sense that it's not a library, but another system communicated via IPC, so it feels much more complicated as a component. (Also, thinking about the growing hurdles with distribution/installation.) On the other hand, @wez pointed out earlier that there are some use cases (especially in context of monorepos) where having a single watch daemon makes absolutely more sense.

To me, it would make sense to study the API and capabilities of Watchman well, but not commit using it as the one and only "watcher backend", but instead use it as an inspiration for an API so that an alternative, library-based watcher backend would be also possible besides of an Watchman-communicating IPC-based one. It also seems to me that the "watcher notifications" part could be decoupled from the snapshot-managing part relatively well, provided that the interface has been thought well enough.

Anyway, some food for thought! Good night.

@ChrisDenton
Copy link
Member

Paths that are super mangled because some legacy program that doesn't know about Unicode wrote them and nothing checked upon them. (I have some experience of programs that expect everything to be ShiftJIS, a Japanese legacy-but-not-dead encoding.)

Have you seen this happen with Windows paths? I'd have thought that for filesystem paths, legacy programs would be using the relevant Windows code page (and associated A functions) rather than the Unicode W functions. The OS will translate these to Unicode itself.

Programmer would surely have to go out of their way to both use the Unicode functions and mangle the output. It could happen but I'd naively assume it would be the result of a mistake rather than for legacy reasons.

@matklad
Copy link
Member Author

matklad commented May 26, 2020

Wow, thanks @golddranks for staying super late and also writing the notes, that's very helpful!

Here are some my take aways from the chat.

  • eager filesystem structure model should work OK. Watchman is eager internally, and it works for huge monorepos. So, dealing only with Rust files shouldn't be a problem
    • eager file contents loading might become problemantic in the future, but there are ways to deal with it (most importantly, by using some kind of binary artifacts (.rlib) for dependencies)
  • eager filesystem structure indeed simplifies some things (noticing that files are created, noticing that files are unlinked to the module tree)
  • no good solution for dynamic list of projects, watchman generally watches the whole monorepo. Dynamic changes of watchman projects structure are bad, and are detected.
    • but we can maintain a two-layer structure -- VFS maintains a flat list of files, and, on top of it, we maintain our SourceRoots
    • SourceRoots are important -- when a user creates new file, we don't want to re-check all the mod statements in stdlib to make sure they don't resolve to the newly introduced file
  • no super great solution for symlinks, but we probably can support common case, and not watching linked dirs should be possible as well
  • no super great solution for case-insensitive file systems
  • even though in Rust modules can refer only to &str named things, you might end up with ^[.rs if you type wrong things on the keyboard, so its important to handle general bag-of-bytes paths.
  • it's better to separate canonical, absolute and relative paths in the type system
  • generated code inside ./target can be problematic (at scale), we might want to tweak Cargo's directory layout a bit.
  • problems of files outside roots and files without paths can be solved by a in-memory layer on top of file-system layer.

My plan is thus:

  • continue with our current eager model
  • try to implement two-layer solution for roots (should probably rename those to FlieTrees) and files outside of the current workspace
  • try to fit a model into some backed :-)
    • we haven't discussed specific backends for file watching
    • my hope is that the answer is "it is a Trait". That is, that the question isn't really important, as we can plug backends later
    • I lean towards using "editor watching/no watching" and watchman as the two default backends.
    • I am also excited to see that there's momentum behind notify 0.5, so switching to that as a default once it matures makes sense
    • That said, I see a lot of reasons behind watchman being a separate service and not a library (as it really polyfills what an OS should do), so I think long-term first-class watchman integration is desirable.

@golddranks
Copy link

golddranks commented May 27, 2020

@matklad How do you feel about allocating and prioritizing this work over some other work in Rust-Analyzer? I feel like actually writing prototypes of new designs and producing PRs for VFS is something that can be reasonably done by volunteers, while you could provide guidance over the design and the API. I'm interested in participating the development in that way, possibly playing around with Watchman, coding a prototype of a new version of VFS and then discuss it. You could then keep working on non-VFS stuff that seems, at least to me, harder to tackle without deeper understanding of the main codebase.

@matklad
Copy link
Member Author

matklad commented Jun 9, 2020

@golddranks urgh, so it took me a long way to get back here :(

Unfortunately, I am pretty bad (yet?) at parallelizing such fundamental changes -- the only algorithm that is known to work here is when someone just dumps 2kloc pull request which implements everything better than I could have done :(

I've started new vfs here https://github.com/matklad/rust-analyzer/tree/vfs (though, the code is in an embarrassing state right now :) )

What would definitely be helpful is bringing the notify library up to production quality (publishing version 0.5). I think @stuhood might be intersted in this as well? From my brief look, they are using notify 0.5-pre right now.

Another useful thing to do would be bulidin an recursive directory watching implementation. I've outlined the API in notify-rs/notify#175 (though I am not entirely sure if it is the right API). This I think can either be implemented as a separate crate on top of either notify 0.4 or notify 0.5, or as an API inside notify 0.5.

@stuhood
Copy link

stuhood commented Jun 9, 2020

What would definitely be helpful is bringing the notify library up to production quality (publishing version 0.5). I think @stuhood might be intersted in this as well? From my brief look, they are using notify 0.5-pre right now.

Polish there would be very much appreciated, yea. Thanks!

Another useful thing to do would be bulidin an recursive directory watching implementation. I've outlined the API in notify-rs/notify#175 (though I am not entirely sure if it is the right API). This I think can either be implemented as a separate crate on top of either notify 0.4 or notify 0.5, or as an API inside notify 0.5.

We expose this kind of API. If it would help for me to expand the description from #3715 (comment) at all, or to extract a demo, let me know. None of the crates are published, but they've been stable for a while.

@matklad
Copy link
Member Author

matklad commented Jun 26, 2020

We've implemented a new API for VFS, which I feel is good enough. The core insight that helped was separating compler-specific concerns (path interning and binning paths into disjoint file sets) from physical file systems concerns (reading and watching files).

Specifically, here's the new interface for file system bits:

https://github.com/rust-analyzer/rust-analyzer/blob/ce06f8d0416d5851264769eb9583ce43d66f0474/crates/vfs/src/loader.rs#L6-L32

The handler encapsulate a separate concurrent actor that can walk, read and watch files upon request. The set of files to crawl is specified in term of absolute paths. We went with eager API, as the model seems simpler, and it's nice to account for files which are not linked into the module tree.

We provide an implementation of this API based on notify 0.5-pre.3:

https://github.com/rust-analyzer/rust-analyzer/blob/ce06f8d0416d5851264769eb9583ce43d66f0474/crates/vfs-notify/src/lib.rs#L22-L62

The impl is known to be buggy around file-watching (file crawling should work OK). As we default to client-side watching, this shouldn't be a problem in practice. Long term, we'd love to pull an off-the-shelf solution for this. When a crate with "watch the set of paths which match the following globs/ignores" appears on crates.io, it should be easy to bridge it to our vfs::loader::Handle API (if anyone plans to write such a crate, note that you don't have to depend on vfs crate! Provide API which makes most sense for you, we'll just bridge it to Handle trait via wrapper type). In fact, watchman_client exposes API which should be bridgable, and we'll probably do that one day (but not today, as client-side watching seems to work well enough is operationally simpler). We do not plan to implement super-correct file watching in rust-analyzer itself, this really should be an independent library.

Thanks everyone for valuable input, it was suuuper helpful ❤️ !

@matklad matklad closed this as completed Jun 26, 2020
@lnicola
Copy link
Member

lnicola commented Jun 26, 2020

Since all the "need to restart RA after adding a dependency" bugs were duplicated to this one, is that supported yet (it doesn't seem to work). Do we want to track it in a different issue?

@matklad
Copy link
Member Author

matklad commented Jun 26, 2020

Good call, opened: #5074

@oooutlk
Copy link

oooutlk commented Apr 19, 2023

When a crate with "watch the set of paths which match the following globs/ignores" appears on crates.io, it should be easy to bridge it to our vfs::loader::Handle API

Now we are in 2023 and crate notify’s version is 5.1.0. Is it mature enough for this task, or any other alternatives?

@matklad
Copy link
Member Author

matklad commented Apr 19, 2023

From the quick look, it seems that notify does not provide this API yet.

The specific thing missing is a unified API for walking and watching. This is the API I think is required here:

fn is_interesting_path(path: &Path) -> bool {
   // Check if this is potentially interesting path (eg, ends with `.rs` and is not in `./target` or `.gitignore`)
}

let watcher = Watcher::new(|p| is_interesting_path(p));
for event in watcher.iter_events() {
   match event {
     // Creating a new watcher _guarantees_ to yield all events for files already
     // on disk if they match the predicate, event if they don't see any changes.
     Event::InitialScan(path) => ...,
     Event::Change(path) => ...,
   }
}

I belive that combining "walk&watch" into a single atomic operation is required for correctness.

notify 5.1.0 does not seem to provide that. watchman should provide that. I also think that folks from the pants build system build something like that as well. I haven't surveyed other libraries, this might be solved on crates.io.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Architecture Big architectural things which we need to figure up-front (or suggestions for rewrites :0) )
Projects
None yet
Development

No branches or pull requests

10 participants