Conversation
34baa70 to
845b4a7
Compare
| dap: Arc<Mutex<Dap>>, | ||
| session_mode: SessionMode, | ||
| ) -> Self { | ||
| let device_context = DeviceContext::new(iopub_tx.clone()); |
There was a problem hiding this comment.
It makes sense to me to remove iopub_tx from DeviceContext now and only pull iopub_tx directly from Console
Over in graphics_device.rs, you can just Console::get().iopub_tx() anytime you need it now!
It feels like this better scopes responsibilities to me
There was a problem hiding this comment.
I feel the opposite. It hides that the device context directly communicates with the frontend via IOPub.
Also ideally we should avoid Console::get() calls as these bypass Rust's ownership model. But then the issue is that the device belongs to console and not the other way around. Cloning the IOPub channel is the right ownership structure from that point of view.
There was a problem hiding this comment.
It hides that the device context directly communicates with the frontend via IOPub
Hmm I see what you mean
Like the DeviceContext struct should have an "include what you use" kind of policy in the struct fields, and by not having iopub_tx in there, we mask what it uses
| Console::get() | ||
| .device_context() | ||
| .set_prerender_settings(params.settings); |
There was a problem hiding this comment.
Is it possible that we could now make DeviceContext have non-Cell internals?
I am disturbed by being able to call get() to then perform a mutable update, rather than being forced to go through get_mut()
There was a problem hiding this comment.
The Cell is actually more reassuring, especially regarding the possibility of reentrancy (flushing calls process_changes which can recurse into graphics device callbacks).
In general get() is not a signal that the state is not mutable, it's only a signal that the state is shared, and can still be mutable e.g. through Cells or other mechanisms.
It's all a bit hand wavy considering that our callbacks might produce multiple active get_mut() at the same time, but adding to the unsoundness would be... unsound ;)
| // Don't try to render a plot if we're currently drawing. | ||
| if self.is_drawing.get() { | ||
| log::trace!("Refusing to render due to `is_drawing`"); | ||
| return; | ||
| } | ||
|
|
||
| // Don't try to render a plot if someone is asking us not to, i.e. `dev.hold()` | ||
| if !self.should_render.get() { | ||
| log::trace!("Refusing to render due to `should_render`"); | ||
| return; | ||
| } |
There was a problem hiding this comment.
I am pretty sure these are important
There was a problem hiding this comment.
Since we now process the requests when R is idle, is_drawing is structurally false, we're never mid-draw.
Regarding should_render (matching dev.hold()), it's true that holds can span across prompts, I can see that happening when developing a plot function interactively. This would require:
- Creating a plot that Positron knows about (opened comm)
- Calling
dev.hold()in the console - Resizing the plot in the frontend UI
So that's a very narrow case. We show the uncommitted visual changes in that case, which seems like a reasonable compromise. The alternative would be to refuse to render, producing a distorted plot in the frontend after resize.
bd54059 to
be02717
Compare
7386726 to
be3d85c
Compare
Better reflects the borrowing model
be3d85c to
74abf66
Compare
|
I also fixed a race between To fix this, I made comm opening blocking. There is a new A nice consequence of this is that the frontend now receives new plots as soon as they are created, rather than at the end of a loop: for (i in 1:3) {
plot(i)
Sys.sleep(1)
}I also noticed that posit-dev/positron#6736 is broken again. And of course this is all now under test coverage. |
DavisVaughan
left a comment
There was a problem hiding this comment.
Two main comments to discuss, other things are minor:
- Not sure about passing
consolethroughhandle_msgand friends - Not sure about
CommEvent::Barriervs passingdonethroughCommEvent::Opened
See below for more on both
My test file seems to be working fine now
| let ready = sel.ready(); | ||
|
|
||
| while let Ok(event) = self.comm_event_rx.try_recv() { | ||
| self.process_comm_event(event); | ||
| } | ||
|
|
||
| if ready == resp_idx { | ||
| break response_rx.recv().unwrap(); | ||
| } |
There was a problem hiding this comment.
So even if sel.ready() returns the execute response as the first "ready" thing, you want to go off and process the comm_events first?
I think I was expecting to see
if ready == resp_idx {
break response_rx.recv().unwrap();
}
right after let ready =
| // Block until Shell has processed the Opened event, ensuring the | ||
| // `comm_open` message is on IOPub before we return. Any updates |
There was a problem hiding this comment.
Just a gut check, but is this really what it ensures? In CommEvent::Opened we do an iopub_tx.send() call, but that doesn't necessarily mean that IOPub has received it by the end of CommEvent::Opened and the following CommEvent::Barrier event is processed.
I'm not sure if that matters.
| dap: Arc<Mutex<Dap>>, | ||
| session_mode: SessionMode, | ||
| ) -> Self { | ||
| let device_context = DeviceContext::new(iopub_tx.clone()); |
There was a problem hiding this comment.
It hides that the device context directly communicates with the frontend via IOPub
Hmm I see what you mean
Like the DeviceContext struct should have an "include what you use" kind of policy in the struct fields, and by not having iopub_tx in there, we mask what it uses
| #[cfg(feature = "testing")] | ||
| pub fn test_init() { |
There was a problem hiding this comment.
Can you put this up with start() and call it test_start()?
| } | ||
|
|
||
| fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext) { | ||
| fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext, _console: &Console) { |
There was a problem hiding this comment.
I'm not sure I agree with passing console through all these handlers
It seems like the only current use that I can see is in impl CommHandler for PlotComm for handle_msg() and handle_close().
But those could just do Console::get().
It doesn't seem like it helps clarify ownership very much, because so many other things in graphics_device.rs also just do Console::get() already. And it seems likely that in handle_rpc() (called by handle_msg()) we could very easily just do another Console::get() rather than passing through the &Console we got from handle_msg(). It seems complex to me to know when to use which pattern.
It also seems like this _console: &Console argument is the reason for all of the new test_init() infra, and overall this just seems like it adds quite a bit of complexity for (to me) not a lot of obvious benefit 😢
There was a problem hiding this comment.
As an example, should on_did_execute_request() gain a console: &Console argument and thread it through process_changes -> process_new_plot -> should_use_dynamic_plots to the Console::get call there?
I don't think so, I feel like it makes sense that we just invoke Console::get() from there as required like we do everywhere else.
But now introducing this new pattern of handle_msg() and friends having a console passed through confuses me and makes me question how we should do things in all the other cases, and I'm not sure that's worth it
| @@ -1271,11 +1137,10 @@ pub(crate) fn on_execute_request( | |||
| #[tracing::instrument(level = "trace", skip_all)] | |||
| pub(crate) fn on_did_execute_request() { | |||
There was a problem hiding this comment.
I think on_did_execute_request() should perhaps be a &self method
It is only called in handle_active_request(), but we have Console there as self, hence we also have DeviceContext, so we can just call self.device_context().on_did_execute_request() from there?
There was a problem hiding this comment.
But I am also a bit weirded out by the ownership implications of all this (both the current state of things and my proposal)
self.device_context().on_did_execute_request() would call DeviceContext::process_changes(), but that goes through process_new_plot() and should_use_dynamic_plots() and you end up accessing the "parent" owner of DeviceContext from there via Console::get(), which feels very mind boggling!
| // Save our new socket. | ||
| // Refcell Safety: Short borrows in the file. | ||
| self.sockets.borrow_mut().insert(id.clone(), socket); | ||
| match Console::get_mut().comm_open_backend(PLOT_COMM_NAME, Box::new(plot_comm)) { |
There was a problem hiding this comment.
I think this is another place where it feeeels weird that DeviceContext is a field of Console, yet refers to Console as well
| } | ||
|
|
||
| pub(crate) struct Console { | ||
| pub struct Console { |
| @@ -699,3 +699,481 @@ fn test_plot_default_size_without_metadata() { | |||
| frontend.recv_iopub_idle(); | |||
There was a problem hiding this comment.
Random note that a cargo build --release is throwing this for me right now
warning: unused import: `libr::SEXP`
--> crates/ark/src/r_task.rs:19:5
|
19 | use libr::SEXP;
| ^^^^^^^^^^
I think this type is only used in debug builds?
| frontend.recv_shell_execute_reply(); | ||
| } | ||
|
|
||
| /// Test that `dev.hold()` suppresses intermediate plot output. |
There was a problem hiding this comment.
It looks like plot(lm(disp ~ drat, data = mtcars)) is working again, but I dont see a backend test for it?
|
Ok let's discuss |
Progress towards #689
Branched from #1099
Addresses posit-dev/positron#12825 (planning to close)
I did not foresee all the simplifications falling out of this one!
Plots no longer live in their own threads, they live on the R thread. That frees up memory since each background thread takes about 2mb of stack space (depending on platform), so 20 open plots would take 40mb of memory.
DeviceContextmoves from a standalonethread_local!into a regular field ofConsole. All access goes throughConsole::get().device_context(). TheCommHandlerContextprovides access toConsoleviactx.console(), so plot handlers reach the device context through a controlled context chain.The old idle-time polling loop (
process_rpc_requestswithcrossbeam::Select) is removed. Plot RPCs are now dispatched synchronously through the shell handler on the R thread. This improves plot latency when pre-renderings require adjustments because we no longer timeout-poll for plot updates (related to Performance for plotting positron#5184).The
GraphicsDeviceNotificationasync channel is removed. Since the UI comm now runs on the R thread (from Run UI comm on the R thread #1099), prerender settings are updated synchronously viaConsole::get().device_context().set_prerender_settings(). This removes quite a bit of plumbing.Fixes a
dev.hold()regression I introduced with pre-renderings (Send pre-renderings of new plots to the frontend #775). We were previously sending pre-renderings unconditionally, now they are held untildev.flush(). This is tested by new integration tests.