Skip to content

Commit 2db9c3a

Browse files
authored
Merge pull request #792 from posit-dev/feature/update-plot-settings
Handle notifications for updated plot render settings
2 parents ec55b57 + 6b0e8a7 commit 2db9c3a

File tree

9 files changed

+397
-284
lines changed

9 files changed

+397
-284
lines changed

crates/amalthea/src/comm/plot_comm.rs

+22-22
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ pub struct PlotResult {
3636
/// The MIME type of the plot data
3737
pub mime_type: String,
3838

39-
/// The policy used to render the plot
40-
pub policy: Option<RenderPolicy>
39+
/// The settings used to render the plot
40+
pub settings: Option<PlotRenderSettings>
4141
}
4242

4343
/// The size of a plot
@@ -50,22 +50,34 @@ pub struct PlotSize {
5050
pub width: i64
5151
}
5252

53-
/// The policy used to render the plot
53+
/// The settings used to render the plot
5454
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
55-
pub struct RenderPolicy {
56-
/// Plot size of the render policy
55+
pub struct PlotRenderSettings {
56+
/// Plot size to render the plot to
5757
pub size: PlotSize,
5858

5959
/// The pixel ratio of the display device
6060
pub pixel_ratio: f64,
6161

62-
/// Format of the render policy
63-
pub format: RenderFormat
62+
/// Format in which to render the plot
63+
pub format: PlotRenderFormat
6464
}
6565

66-
/// Possible values for RenderFormat
66+
/// Possible values for PlotUnit
67+
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, strum_macros::Display)]
68+
pub enum PlotUnit {
69+
#[serde(rename = "pixels")]
70+
#[strum(to_string = "pixels")]
71+
Pixels,
72+
73+
#[serde(rename = "inches")]
74+
#[strum(to_string = "inches")]
75+
Inches
76+
}
77+
78+
/// Possible values for PlotRenderFormat
6779
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, strum_macros::Display)]
68-
pub enum RenderFormat {
80+
pub enum PlotRenderFormat {
6981
#[serde(rename = "png")]
7082
#[strum(to_string = "png")]
7183
Png,
@@ -87,18 +99,6 @@ pub enum RenderFormat {
8799
Tiff
88100
}
89101

90-
/// Possible values for PlotUnit
91-
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, strum_macros::Display)]
92-
pub enum PlotUnit {
93-
#[serde(rename = "pixels")]
94-
#[strum(to_string = "pixels")]
95-
Pixels,
96-
97-
#[serde(rename = "inches")]
98-
#[strum(to_string = "inches")]
99-
Inches
100-
}
101-
102102
/// Parameters for the Render method.
103103
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
104104
pub struct RenderParams {
@@ -110,7 +110,7 @@ pub struct RenderParams {
110110
pub pixel_ratio: f64,
111111

112112
/// The requested plot format
113-
pub format: RenderFormat,
113+
pub format: PlotRenderFormat,
114114
}
115115

116116
/**

crates/amalthea/src/comm/ui_comm.rs

+20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
use serde::Deserialize;
1212
use serde::Serialize;
13+
use super::plot_comm::PlotRenderSettings;
1314

1415
/// Items in Params
1516
pub type Param = serde_json::Value;
@@ -97,6 +98,13 @@ pub struct Range {
9798
pub end: Position
9899
}
99100

101+
/// Parameters for the DidChangePlotsRenderSettings method.
102+
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
103+
pub struct DidChangePlotsRenderSettingsParams {
104+
/// Plot rendering settings.
105+
pub settings: PlotRenderSettings,
106+
}
107+
100108
/// Parameters for the CallMethod method.
101109
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
102110
pub struct CallMethodParams {
@@ -290,6 +298,15 @@ pub struct ShowHtmlFileParams {
290298
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
291299
#[serde(tag = "method", content = "params")]
292300
pub enum UiBackendRequest {
301+
/// Notification that the settings to render a plot (i.e. the plot size)
302+
/// have changed.
303+
///
304+
/// Typically fired when the plot component has been resized by the user.
305+
/// This notification is useful to produce accurate pre-renderings of
306+
/// plots.
307+
#[serde(rename = "did_change_plots_render_settings")]
308+
DidChangePlotsRenderSettings(DidChangePlotsRenderSettingsParams),
309+
293310
/// Run a method in the interpreter and return the result to the frontend
294311
///
295312
/// Unlike other RPC methods, `call_method` calls into methods implemented
@@ -306,6 +323,9 @@ pub enum UiBackendRequest {
306323
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
307324
#[serde(tag = "method", content = "result")]
308325
pub enum UiBackendReply {
326+
/// Unused response to notification
327+
DidChangePlotsRenderSettingsReply(),
328+
309329
/// The method result
310330
CallMethodReply(CallMethodResult),
311331

crates/ark/src/interface.rs

+46-33
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ use amalthea::Error;
5252
use anyhow::*;
5353
use bus::Bus;
5454
use crossbeam::channel::bounded;
55-
use crossbeam::channel::unbounded;
5655
use crossbeam::channel::Receiver;
5756
use crossbeam::channel::Sender;
5857
use harp::command::r_command;
@@ -89,6 +88,7 @@ use regex::Regex;
8988
use serde_json::json;
9089
use stdext::result::ResultOrLog;
9190
use stdext::*;
91+
use tokio::sync::mpsc::UnboundedReceiver as AsyncUnboundedReceiver;
9292
use uuid::Uuid;
9393

9494
use crate::dap::dap::DapBackendEvent;
@@ -107,6 +107,7 @@ use crate::lsp::state_handlers::ConsoleInputs;
107107
use crate::modules;
108108
use crate::modules::ARK_ENVS;
109109
use crate::plots::graphics_device;
110+
use crate::plots::graphics_device::GraphicsDeviceNotification;
110111
use crate::r_task;
111112
use crate::r_task::BoxFuture;
112113
use crate::r_task::RTask;
@@ -321,7 +322,7 @@ impl RMain {
321322
/// Sets up the main R thread, initializes the `R_MAIN` singleton,
322323
/// and starts R. Does not return!
323324
/// SAFETY: Must be called only once. Enforced with a panic.
324-
pub fn start(
325+
pub(crate) fn start(
325326
r_args: Vec<String>,
326327
startup_file: Option<String>,
327328
comm_manager_tx: Sender<CommManagerEvent>,
@@ -334,6 +335,7 @@ impl RMain {
334335
dap: Arc<Mutex<Dap>>,
335336
session_mode: SessionMode,
336337
default_repos: DefaultRepos,
338+
graphics_device_rx: AsyncUnboundedReceiver<GraphicsDeviceNotification>,
337339
) {
338340
// Set the main thread ID.
339341
// Must happen before doing anything that checks `RMain::on_main_thread()`,
@@ -345,9 +347,7 @@ impl RMain {
345347
};
346348
}
347349

348-
// Channels to send/receive tasks from auxiliary threads via `RTask`s
349-
let (tasks_interrupt_tx, tasks_interrupt_rx) = unbounded::<RTask>();
350-
let (tasks_idle_tx, tasks_idle_rx) = unbounded::<RTask>();
350+
let (tasks_interrupt_rx, tasks_idle_rx) = r_task::take_receivers();
351351

352352
R_MAIN.set(UnsafeCell::new(RMain::new(
353353
tasks_interrupt_rx,
@@ -364,12 +364,6 @@ impl RMain {
364364

365365
let main = RMain::get_mut();
366366

367-
// Initialize the GD context on this thread
368-
graphics_device::init_graphics_device(
369-
main.get_comm_manager_tx().clone(),
370-
main.get_iopub_tx().clone(),
371-
);
372-
373367
let mut r_args = r_args.clone();
374368

375369
// Record if the user has requested that we don't load the site/user level R profiles
@@ -445,12 +439,6 @@ impl RMain {
445439
.or_log_error(&format!("Failed to source startup file '{file}' due to"));
446440
}
447441

448-
// R and ark are now set up enough to allow interrupt-time and idle-time tasks
449-
// to be sent through. Idle-time tasks will be run once we enter
450-
// `read_console()` for the first time. Interrupt-time tasks could be run
451-
// sooner if we hit a check-interrupt before then.
452-
r_task::initialize(tasks_interrupt_tx, tasks_idle_tx);
453-
454442
// Initialize support functions (after routine registration, after r_task initialization)
455443
match modules::initialize() {
456444
Err(err) => {
@@ -493,6 +481,18 @@ impl RMain {
493481
);
494482
Self::complete_initialization(main.banner.take(), kernel_init_tx);
495483

484+
// Initialize the GD context on this thread.
485+
// Note that we do it after init is complete to avoid deadlocking
486+
// integration tests by spawning an async task. The deadlock is caused
487+
// by https://github.com/posit-dev/ark/blob/bd827e735970ca17102aeddfbe2c3ccf26950a36/crates/ark/src/r_task.rs#L261.
488+
// We should be able to remove this escape hatch in `r_task()` by
489+
// instantiating an `RMain` in unit tests as well.
490+
graphics_device::init_graphics_device(
491+
main.get_comm_manager_tx().clone(),
492+
main.get_iopub_tx().clone(),
493+
graphics_device_rx,
494+
);
495+
496496
// Now that R has started and libr and ark have fully initialized, run site and user
497497
// level R profiles, in that order
498498
if !ignore_site_r_profile {
@@ -810,10 +810,16 @@ impl RMain {
810810
let tasks_interrupt_rx = self.tasks_interrupt_rx.clone();
811811
let tasks_idle_rx = self.tasks_idle_rx.clone();
812812

813+
// Process R's polled events regularly while waiting for console input.
814+
// We used to poll every 200ms but that lead to visible delays for the
815+
// processing of plot events.
816+
let polled_events_rx = crossbeam::channel::tick(Duration::from_millis(50));
817+
813818
let r_request_index = select.recv(&r_request_rx);
814819
let stdin_reply_index = select.recv(&stdin_reply_rx);
815820
let kernel_request_index = select.recv(&kernel_request_rx);
816821
let tasks_interrupt_index = select.recv(&tasks_interrupt_rx);
822+
let polled_events_index = select.recv(&polled_events_rx);
817823

818824
// Don't process idle tasks in browser prompts. We currently don't want
819825
// idle tasks (e.g. for srcref generation) to run when the call stack is
@@ -859,18 +865,7 @@ impl RMain {
859865
}
860866
}
861867

862-
let oper = select.select_timeout(Duration::from_millis(200));
863-
864-
let Ok(oper) = oper else {
865-
// We hit a timeout. Process idle events because we need to
866-
// pump the event loop while waiting for console input.
867-
//
868-
// Alternatively, we could try to figure out the file
869-
// descriptors that R has open and select() on those for
870-
// available data?
871-
unsafe { Self::process_idle_events() };
872-
continue;
873-
};
868+
let oper = select.select();
874869

875870
match oper.index() {
876871
// We've got an execute request from the frontend
@@ -910,6 +905,12 @@ impl RMain {
910905
self.handle_task(task);
911906
},
912907

908+
// It's time to run R's polled events
909+
i if i == polled_events_index => {
910+
let _ = oper.recv(&polled_events_rx).unwrap();
911+
Self::process_idle_events();
912+
},
913+
913914
i => log::error!("Unexpected index in Select: {i}"),
914915
}
915916
}
@@ -1845,6 +1846,14 @@ impl RMain {
18451846

18461847
/// Invoked by the R event loop
18471848
fn polled_events(&mut self) {
1849+
// Don't process tasks until R is fully initialized
1850+
if !Self::is_initialized() {
1851+
if !self.tasks_interrupt_rx.is_empty() {
1852+
log::trace!("Delaying execution of interrupt task as R isn't initialized yet");
1853+
}
1854+
return;
1855+
}
1856+
18481857
// Skip running tasks if we don't have 128KB of stack space available.
18491858
// This is 1/8th of the typical Windows stack space (1MB, whereas macOS
18501859
// and Linux have 8MB).
@@ -1863,23 +1872,27 @@ impl RMain {
18631872
}
18641873
}
18651874

1866-
unsafe fn process_idle_events() {
1875+
fn process_idle_events() {
18671876
// Process regular R events. We're normally running with polled
18681877
// events disabled so that won't run here. We also run with
18691878
// interrupts disabled, so on Windows those won't get run here
18701879
// either (i.e. if `UserBreak` is set), but it will reset `UserBreak`
18711880
// so we need to ensure we handle interrupts right before calling
18721881
// this.
1873-
R_ProcessEvents();
1882+
unsafe { R_ProcessEvents() };
18741883

18751884
crate::sys::interface::run_activity_handlers();
18761885

18771886
// Run pending finalizers. We need to do this eagerly as otherwise finalizers
18781887
// might end up being executed on the LSP thread.
18791888
// https://github.com/rstudio/positron/issues/431
1880-
R_RunPendingFinalizers();
1889+
unsafe { R_RunPendingFinalizers() };
18811890

1882-
// Check for Positron render requests
1891+
// Check for Positron render requests.
1892+
//
1893+
// TODO: This should move to a spawned task that'd be woken up by
1894+
// incoming messages on plot comms. This way we'll prevent the delays
1895+
// introduced by timeout-based event polling.
18831896
graphics_device::on_process_idle_events();
18841897
}
18851898

0 commit comments

Comments
 (0)