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

watcher refactor #3471

Merged
merged 17 commits into from
Apr 15, 2024
Merged

watcher refactor #3471

merged 17 commits into from
Apr 15, 2024

Conversation

Byron
Copy link
Collaborator

@Byron Byron commented Apr 9, 2024

This PR attemps to minimize the watcher implementation to only the needed complexity.

Based on the analysis in #3447.

Tasks

  • (warmup): try_from() implementation becomes a constructor method.
  • See if consuming run() is accidental (it was)
  • reorganize dispatcher to work without extra objects and without cancellation token (channels are all that's needed for producers to shut down when consumers shut down, and the dispatcher is a producer)
  • See if each Handler needs to be managed - are they used and if so, how? Those who aren't shouldn't be managed.
    • It seems they are only managed because they check if they are managed - a catch22?
    • Careful: this could break the app, but if it does, hopefully thoroughly so it's obvious.
  • Remove Clone from Watcher by allowing communication to it using a channel
    • That way, the channel can be Clone, but the watcher doesn't have to be.
      This is a precondition for removing all the interior Send+Sync and locking protections,
      while the Watcher will have to watch in the background and thus be in a loop.
  • try to implement each Handler as function or method.
  • remove the event loop by making relevant calls directly (as much as possible)
    • remove last pure suffixed function and fixup tests to allow dissolve the last remaining private events
    • fix tests - needs tauri support, which works with mocks
  • fix as many remaining TODO(ST) as possible (only 3 remaining, 2 of which I might fix here)
  • make events::Event just Event
  • check usage (also after changing of .gitignore at app runtime) of ignore-filter to see if it can be removed/improved to be faster
  • try to parallelise delta computation
    • Performance test was git clone https://github.com/BurntSushi/ripgrep, then gco @~200 -- crates/. After each run, the worktree was reset to @.
    • It's definitely working with granularity of a single file, and it's way faster than the single-threaded version
  • validate that it is not slower than the application on master
    • It's not necessarily faster than the ultra-parallel auto-parallelizing version of the watcher in master. Something I don't understand is that a single file here costs 12s runtime on a single thread, yet master is done way earlier than that (or at least, logging stops earlier). It's like it is not seeing all the file or something else is going on that causes it to skip computations maybe?
  • Create issues for application bugs after re-validating them
  • Only watch one project at a time

Notes for the Reviewer

  • The delta computation consumes the most CPU, and if it's truly unused, I think it should be removed from the handler at least.
  • I removed the rate-limited, as it never kicked in during my testing due to the handling of multiple paths at once. This was an issue to me as well as simply not calculating something would have meant that sometimes, it's not arriving at the correct result.
  • I am pretty sure that I never got to test the fetch and push GB repo functionality, maybe much more. Better test everything that is triggered by watchers.
  • LOG_LEVEL=info should probably be preferred when testing the performance of git checkout @~100 -- src/ or similar as it will still emit a lot of debug messages that are buried deeply in the core part. This makes it seem like a lot is happening, even though that's not the case anymore in comparison to the before-times.
  • I adjusted the log level of a few traces in core to allow launching the application in DEBUG mode without being spammed with low-level information. Instead, DEBUG is now used for select information, and everything else was demoted to the TRACE level. It's probably the way things go, what's useful in DEBUG one day is better as TRACE in another as the focus changes.

Application Bugs 📋

These bugs were encountered during testing.

Out of Scope but TODO

  • git::credential::Helpers::from_path() is used in tests, but depends on the actual HOME environment variables, isn't isolated
    • Maybe check other from_path() implementations as well.

My Notes

Definitely not a mandatory read to review this PR.

  • There seem to be redundant calls to is_path_ignored() in many places, given that the dispatcher filter already avoids ignored files (and can do so much more efficiently, but we must assure that it always gets most recent information .gitignore files as the watcher is long-running).
  • In theory, since it's going to be communicating with channels to the outside, there should be no fear of parallelism at all, so all Handlers should have no need. One can hope they are isolated which they are probably not as they are all managed in tauri. This should probably be rolled back as much as possible.
  • Send+Sync + shared state and Clone are poison, and a way to feel unsafe even without the keyword. In such an environment, one has no garantuees anymore, so one will want all code not to be dealing with that.
    • This is probably the entrypoint to unravelling Send+Sync in core as well, even though it might also be a good hint at how these core instances can be used. My feeling is that many will need a tauri-specific wrapper, so that core can be pure.
  • There is a large amount of boilerplate just to deal with all the blanket 'protections' needed for Send+Sync, many handlers are just a couple of lines. One handler is just sending an event, effectively.
  • The current event queue system is actually very cleverly multi-threading everything. Each event coming in is processed in its own (or pooled) thread, and as each even can spawn new events, these will equally be queued and run in parallel. This is why everything is Send + Sync in the watcher.
  • Seeing the .git/GB_Flush file, I wonder if this file-based notification system is truly necessary, or if that could be kept in tauri. Maybe the plan is to support multiple windows to be open on the same repository (rather than prevent it), which probably would make this file-based approach necessary, and maybe even the most simple solution if filesystem events can be assumed to be reliable.
  • There definitely is some business logic in the watcher that probably wants to move to core at some point,.
  • There is generally a lot of rather generic string-only context() calls and it feels these are 'just there to be there' even though the callee probably could deliver decent error messages from the beginning (and maybe already does so).
  • Tests are definitely easier to write with the Events system as side-effects are nicely decoupled via returned events. Now that everything is coupled again, it becomes clear how much state is required for certain functions, making the initial test setup more involved.

@Byron Byron force-pushed the watcher-refactor branch 24 times, most recently from b025e4e to fd0ee99 Compare April 13, 2024 21:08
Byron added 6 commits April 13, 2024 23:09
`try_new()` here is used as constructor, which is what `new` is for
with less boilerplate.
That way, all objects go away and it will be nothing more than a task
around a channel.
As `Watcher` really adds nothing.
@Byron Byron force-pushed the watcher-refactor branch 4 times, most recently from 070e3e1 to cd8e450 Compare April 14, 2024 13:21
@Byron Byron force-pushed the watcher-refactor branch 2 times, most recently from 50565d7 to 1cb6451 Compare April 14, 2024 16:49
@Byron Byron marked this pull request as ready for review April 14, 2024 17:03
Byron added 6 commits April 15, 2024 07:11
The idea is that we don't parallelize over a channel anymore, but
instead just process filesystem events, one at a time.

This would allow each handler to become a function that gets its
state passed, and makes all the necessary calls verbatim, which
in turn makes it easy to follow what's happening.

If anything becomes to slow due to the serialization of event processing,
selective parallelization can be re-added.
That way, we get `tauri::Event`, without the somewhat confusing
module  name `events`.
Previously, each file change both in `.git` as well as in the worktree would
cause a complete recomputation. This computation included opening a git
repository at least once (probaby more often), to make an 'is-ignored' check.

The latter is very expensive in `git2` and gets more expensive the more
files there are.

Now the repository is opened when needed, and we re-use it for all applicable
file paths.
Previously it would watch every registered project, which could incur more work
on all parts of the application than necessary.

Now UI sends an event that indicates which project is active, allowing the
watch to be setup in that very moment. It's worth noting that the previously
watched project is automatically deregistered.
@Byron Byron force-pushed the watcher-refactor branch from 1cb6451 to e2ef2dc Compare April 15, 2024 05:25
@@ -29,6 +29,7 @@ backoff = "0.4.0"
backtrace = { version = "0.3.71", optional = true }
chrono = { version = "0.4.37", features = ["serde"] }
console-subscriber = "0.2.0"
crossbeam-channel = "0.5.12"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for crossbeam channels vs using tokio's channels?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, tokio channels might be usable here and I didn't try. It would certainly be preferable if it worked.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try tokio then, lmk if it doesn't work for some reason. Just trying to reduce our number of dependencies.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did take a look and realised that tokio::sync::broadcast is indeed able to provide the multi-multi case. Probably last time I didn't really look there though since it requires async, whereas this code is all non-async.

crossbeam-channel was previously used in the dependency graph by 5 other dependencies and isn't adding to the compile time.

Do you still think crossbeam-channel should be replaced with tokio::sync::broadcast? If so, I'd probably go with block_on where needed as this implementation relies on std::thread::scope(). Turning it into async-proper for parallelisation would be more effort.

Copy link
Contributor

@Qix- Qix- Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably fine, we can move more toward async over time.

Also TIL about std::thread::scope() - neat!

@Byron
Copy link
Collaborator Author

Byron commented Apr 15, 2024

Thanks for starting the review, @Qix- !

I recommend to gh pr checkout 3471 this PR and push fixes and changes directly into it. For bigger questions, I will answer everything that might be coming up here. Thanks again.

PS: @mtsgrd I replied on top of your replies in some comments as what I saw was out of date, not because I thought I had to add to them.

Byron added 3 commits April 15, 2024 16:23
The `pure` functions were from a time where a `Handler` couldn't be instantiated
in full for tests, but that is not the case anymore, so there isn't any use
for the added complexity.
- turn `static` into `const`
Copy link
Collaborator Author

@Byron Byron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! I think I have addressed all comments thus far, please let me know if anything else comes up.

@Qix- Qix- merged commit f9f1f3d into gitbutlerapp:master Apr 15, 2024
34 checks passed
@Qix-
Copy link
Contributor

Qix- commented Apr 15, 2024

Thanks!

@Byron Byron deleted the watcher-refactor branch April 15, 2024 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants