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

red-knot: Introduce program.check #11148

Merged
merged 5 commits into from
Apr 27, 2024
Merged

red-knot: Introduce program.check #11148

merged 5 commits into from
Apr 27, 2024

Conversation

MichaReiser
Copy link
Member

@MichaReiser MichaReiser commented Apr 25, 2024

This PR refactors the main.rs by:

  • Extracting the check logic and moving it to program.check. There's some complication involved because we want the host environment to control how checks are scheduled (concurrently, or on the same thread). What I have now kind of works and isn't too much of a mess, but I feel like I manually implemented Futures. @BurntSushi do you know if it's possible to use Futures with out async? Yeah, it sounds stupid, hehe but what I want is something that starts some work and an orchestration thread can later poll the results without knowing if the computation runs on the same or another thread.
  • I refactored the main loop to prevent that we create a new orchestration thread for every loop. Instead, the orchestration thread is now started once and it sends messages to the main thread, telling it what the next operation is that it should perform (react to file changes, print diagnostics, check the program)

@MichaReiser MichaReiser changed the base branch from main to red-knot April 25, 2024 16:31
@MichaReiser MichaReiser force-pushed the red-knot-program-check branch 2 times, most recently from c852acb to cd17300 Compare April 25, 2024 18:07
Copy link
Contributor

github-actions bot commented Apr 25, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@MichaReiser MichaReiser force-pushed the red-knot-program-check branch from cd17300 to 236e70a Compare April 26, 2024 07:02
@@ -32,7 +32,7 @@ impl Files {
self.inner.read().try_get(path)
}

// TODO Can we avoid using an `Arc` here? salsa can return references for some reason.
Copy link
Member Author

Choose a reason for hiding this comment

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

We could by using an unsafe transmute similar to salsa.

@MichaReiser MichaReiser changed the title red knot program check red-knot: Introduce program.check Apr 26, 2024
@MichaReiser MichaReiser added the internal An internal refactor or improvement label Apr 26, 2024
@MichaReiser MichaReiser marked this pull request as ready for review April 26, 2024 07:10
@MichaReiser MichaReiser force-pushed the red-knot-program-check branch 2 times, most recently from ce9962f to 59fc0e7 Compare April 26, 2024 07:41
@MichaReiser MichaReiser requested a review from carljm April 26, 2024 08:58
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

This is great! Much easier to follow, I think I got my head around all of it.

receiver: crossbeam_channel::Receiver<OrchestratorMessage>,
}

impl Orchestrator {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let me see if I understand the division of responsibilities between MainLoop and Orchestrator threads. MainLoop does the actual checking, using a rayon thread pool to parallelize checking. The job of Orchestrator is to a) collect file changes from a notification source, debounce them, and then hand them off to MainLoop, which applies them to the Program and then re-checks, and b) receive check results (diagnostics) from the rayon thread running the actual check, and hand those off to MainLoop for display?

The term "Orchestrator" initially had me thinking it would actually coordinate the checking work, but it doesn't really do that; that's RayonCheckScheduler. Orchestrator is really just orchestrating at the higher level of check requests and results.

It's not clear to me yet how these pieces will fit together in an editor context when we are handling requests much smaller than "check the whole program."

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that's correct. The Orchestrator orchestrates the individual moving pieces. It needs to run on it' own thread so that it can react to incoming events. But it's the main loop that does the actual work

It's not clear to me yet how these pieces will fit together in an editor context when we are handling requests much smaller than "check the whole program."

program.check should only recheck files that need rechecking since the last program.check call. That's what needs to be called inside of the LSPs pull diagnostics/push_diagnostics handlers.

The term "Orchestrator" initially had me thinking it would actually coordinate the checking work, but it doesn't really do that; that's RayonCheckScheduler. Orchestrator is really just orchestrating at the higher level of check requests and results.

I'm open to better names for Orchestrator.


started_analysis.fetch_add(1, Ordering::SeqCst);
OrchestratorMessage::CheckProgramCompleted(diagnostics) => {
self.pending_analysis
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess pending_analysis represents the fact that we know MainLoop is currently running an analysis. Would it make sense for each analysis request to have a unique ID that gets carried along with all the relevant messages (CheckProgram, CheckProgramStarted, CheckProgramCompleted, CheckProgramCancelled, etc) and also stored in the PendingAnalysisState, just to help us validate our assumptions about what corresponds to what, rather than implicitly relying on the two state machines always having their states matched up correctly?

Copy link
Member Author

@MichaReiser MichaReiser Apr 27, 2024

Choose a reason for hiding this comment

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

I consider it a bug if the state machines are out of sync and would prefer if we panic over trying to make it work somehow (and leak memory).

Copy link
Contributor

Choose a reason for hiding this comment

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

That's reasonable. I wasn't suggesting we try to recover, more just that this could help clarify what happened in the bug case. But maybe tracing is sufficient for that.

})
.unwrap();

rayon::in_place_scope(|scope| {
Copy link
Contributor

Choose a reason for hiding this comment

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

This blocks until checking is all done, right? So then we'll go to the next tick and probably find a CheckCompleted message waiting for us already? (Or else just a new ApplyChanges and CheckProgram, if this check got cancelled.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that's correct. Although the ApplyChanges might gets debounced first to wait for new incoming file change message. It would be nice if this could all be in a single place but the challenge I faced is that we need to have a single "input" on which the orchestrator thread can wait on (suspend).

}

#[tracing::instrument(level = "debug", skip(self, context))]
fn check_file_with_context(
Copy link
Contributor

Choose a reason for hiding this comment

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

This name seems confusing -- it doesn't really clarify how it is different from check_file. It's more like actually_check_file. The fact that it takes a context is clear from the signature, doesn't really add anything in the name. Maybe just do_check?

Right now it doesn't even really need the context (CheckFileTask::run could check for cancellation), but in future when we do more complex and slower checks, we'll probably want to do more cancellation checks internally here, not just one at the start?

Copy link
Member Author

Choose a reason for hiding this comment

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

My idea is to make cancellation checks on every query, and expose an API so that we can perform checks even inside of queries.

Base automatically changed from red-knot to main April 27, 2024 08:34
@MichaReiser MichaReiser force-pushed the red-knot-program-check branch from b73a81f to aca0d3d Compare April 27, 2024 08:50
@MichaReiser MichaReiser force-pushed the red-knot-program-check branch from aca0d3d to 636ca68 Compare April 27, 2024 08:54
@MichaReiser MichaReiser enabled auto-merge (squash) April 27, 2024 08:54
@MichaReiser MichaReiser merged commit 61c97a0 into main Apr 27, 2024
18 checks passed
@MichaReiser MichaReiser deleted the red-knot-program-check branch April 27, 2024 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
internal An internal refactor or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants