Skip to content

Commit 6b8833e

Browse files
authored
Merge pull request #719 from posit-dev/task/reparse-lazily
Generate srcrefs lazily
2 parents 2558a23 + 9ec264e commit 6b8833e

File tree

5 files changed

+182
-87
lines changed

5 files changed

+182
-87
lines changed

.zed/settings.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"languages": {
3+
"R": {
4+
"hard_tabs": false,
5+
"tab_size": 4
6+
}
7+
}
8+
}

crates/ark/src/interface.rs

+73-32
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ use crossbeam::channel::bounded;
5353
use crossbeam::channel::unbounded;
5454
use crossbeam::channel::Receiver;
5555
use crossbeam::channel::Sender;
56-
use crossbeam::select;
5756
use harp::command::r_command;
5857
use harp::command::r_home_setup;
5958
use harp::environment::r_ns_env;
@@ -103,6 +102,7 @@ use crate::lsp::main_loop::KernelNotification;
103102
use crate::lsp::main_loop::TokioUnboundedSender;
104103
use crate::lsp::state_handlers::ConsoleInputs;
105104
use crate::modules;
105+
use crate::modules::ARK_ENVS;
106106
use crate::plots::graphics_device;
107107
use crate::r_task;
108108
use crate::r_task::BoxFuture;
@@ -443,7 +443,7 @@ impl RMain {
443443
}
444444

445445
// Register all hooks once all modules have been imported
446-
let hook_result = RFunction::from(".ps.register_all_hooks").call();
446+
let hook_result = RFunction::from("register_hooks").call_in(ARK_ENVS.positron_ns);
447447
if let Err(err) = hook_result {
448448
log::error!("Error registering some hooks: {err:?}");
449449
}
@@ -753,6 +753,32 @@ impl RMain {
753753
}
754754
}
755755

756+
let mut select = crossbeam::channel::Select::new();
757+
758+
// Cloning is necessary to avoid a double mutable borrow error
759+
let r_request_rx = self.r_request_rx.clone();
760+
let stdin_reply_rx = self.stdin_reply_rx.clone();
761+
let kernel_request_rx = self.kernel_request_rx.clone();
762+
let tasks_interrupt_rx = self.tasks_interrupt_rx.clone();
763+
let tasks_idle_rx = self.tasks_idle_rx.clone();
764+
765+
let r_request_index = select.recv(&r_request_rx);
766+
let stdin_reply_index = select.recv(&stdin_reply_rx);
767+
let kernel_request_index = select.recv(&kernel_request_rx);
768+
let tasks_interrupt_index = select.recv(&tasks_interrupt_rx);
769+
770+
// Don't process idle tasks in browser prompts. We currently don't want
771+
// idle tasks (e.g. for srcref generation) to run when the call stack is
772+
// empty. We could make this configurable though if needed, i.e. some
773+
// idle tasks would be able to run in the browser. Those should be sent
774+
// to a dedicated channel that would always be included in the set of
775+
// recv channels.
776+
let tasks_idle_index = if info.browser {
777+
None
778+
} else {
779+
Some(select.recv(&tasks_idle_rx))
780+
};
781+
756782
loop {
757783
// If an interrupt was signaled and we are in a user
758784
// request prompt, e.g. `readline()`, we need to propagate
@@ -777,17 +803,31 @@ impl RMain {
777803
// to be handled in a blocking way to ensure subscribers are
778804
// notified before the next incoming message is processed.
779805

780-
// First handle execute requests outside of `select!` to ensure they
781-
// have priority. `select!` chooses at random.
782-
if let Ok(req) = self.r_request_rx.try_recv() {
806+
// First handle execute requests outside of `select` to ensure they
807+
// have priority. `select` chooses at random.
808+
if let Ok(req) = r_request_rx.try_recv() {
783809
if let Some(input) = self.handle_execute_request(req, &info, buf, buflen) {
784810
return input;
785811
}
786812
}
787813

788-
select! {
789-
// Wait for an execution request from the frontend.
790-
recv(self.r_request_rx) -> req => {
814+
let oper = select.select_timeout(Duration::from_millis(200));
815+
816+
let Ok(oper) = oper else {
817+
// We hit a timeout. Process events because we need to
818+
// pump the event loop while waiting for console input.
819+
//
820+
// Alternatively, we could try to figure out the file
821+
// descriptors that R has open and select() on those for
822+
// available data?
823+
unsafe { Self::process_events() };
824+
continue;
825+
};
826+
827+
match oper.index() {
828+
// We've got an execute request from the frontend
829+
i if i == r_request_index => {
830+
let req = oper.recv(&r_request_rx);
791831
let Ok(req) = req else {
792832
// The channel is disconnected and empty
793833
return ConsoleResult::Disconnected;
@@ -796,35 +836,33 @@ impl RMain {
796836
if let Some(input) = self.handle_execute_request(req, &info, buf, buflen) {
797837
return input;
798838
}
799-
}
839+
},
800840

801841
// We've got a reply for readline
802-
recv(self.stdin_reply_rx) -> reply => {
803-
return self.handle_input_reply(reply.unwrap(), buf, buflen);
804-
}
842+
i if i == stdin_reply_index => {
843+
let reply = oper.recv(&stdin_reply_rx).unwrap();
844+
return self.handle_input_reply(reply, buf, buflen);
845+
},
805846

806847
// We've got a kernel request
807-
recv(self.kernel_request_rx) -> req => {
808-
self.handle_kernel_request(req.unwrap(), &info);
809-
}
848+
i if i == kernel_request_index => {
849+
let req = oper.recv(&kernel_request_rx).unwrap();
850+
self.handle_kernel_request(req, &info);
851+
},
810852

811-
// A task woke us up, start next loop tick to yield to it
812-
recv(self.tasks_interrupt_rx) -> task => {
813-
self.handle_task_interrupt(task.unwrap());
814-
}
815-
recv(self.tasks_idle_rx) -> task => {
816-
self.handle_task(task.unwrap());
817-
}
853+
// An interrupt task woke us up
854+
i if i == tasks_interrupt_index => {
855+
let task = oper.recv(&tasks_interrupt_rx).unwrap();
856+
self.handle_task_interrupt(task);
857+
},
818858

819-
// Wait with a timeout. Necessary because we need to
820-
// pump the event loop while waiting for console input.
821-
//
822-
// Alternatively, we could try to figure out the file
823-
// descriptors that R has open and select() on those for
824-
// available data?
825-
default(Duration::from_millis(200)) => {
826-
unsafe { Self::process_events() };
827-
}
859+
// An idle task woke us up
860+
i if Some(i) == tasks_idle_index => {
861+
let task = oper.recv(&tasks_idle_rx).unwrap();
862+
self.handle_task(task);
863+
},
864+
865+
i => log::error!("Unexpected index in Select: {i}"),
828866
}
829867
}
830868
}
@@ -2056,7 +2094,10 @@ fn do_resource_namespaces() -> bool {
20562094
let opt: Option<bool> = r_null_or_try_into(harp::get_option("ark.resource_namespaces"))
20572095
.ok()
20582096
.flatten();
2059-
opt.unwrap_or(true)
2097+
2098+
// By default we don't eagerly resource namespaces to avoid increased memory usage.
2099+
// https://github.com/posit-dev/positron/issues/5050
2100+
opt.unwrap_or(false)
20602101
}
20612102

20622103
/// Are we auto-printing?

crates/ark/src/modules/positron/hooks.R

+42-46
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,66 @@
11
#
22
# hooks.R
33
#
4-
# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
4+
# Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
55
#
66
#
77

8-
#' @export
9-
.ps.register_utils_hook <- function(name, hook, namespace = FALSE) {
10-
if (namespace) {
11-
hook_namespace <- hook
12-
} else {
13-
hook_namespace <- NULL
14-
}
15-
16-
pkg_hook(
17-
pkg = "utils",
18-
name = name,
19-
hook = hook,
20-
hook_namespace = hook_namespace
21-
)
8+
register_hooks <- function() {
9+
rebind("utils", "View", .ps.view_data_frame, namespace = TRUE)
10+
rebind("base", "debug", new_ark_debug(base::debug), namespace = TRUE)
11+
rebind("base", "debugonce", new_ark_debug(base::debugonce), namespace = TRUE)
12+
register_getHook_hook()
2213
}
2314

24-
# Wrapper to contain the definition of all hooks we want to register
25-
#' @export
26-
.ps.register_all_hooks <- function() {
27-
.ps.register_utils_hook("View", .ps.view_data_frame, namespace = TRUE)
28-
register_getHook_hook()
15+
rebind <- function(pkg, name, value, namespace = FALSE) {
16+
pkg_bind(
17+
pkg = pkg,
18+
name = name,
19+
value = value
20+
)
21+
22+
if (namespace) {
23+
ns_bind(
24+
pkg = pkg,
25+
name = name,
26+
value = value
27+
)
28+
}
2929
}
3030

3131
#' Override a function within an attached package
3232
#'
3333
#' Assumes the package is attached, typically used for base packages like base or utils.
34-
#' - `hook` will replace the binding for unnamespaced calls.
35-
#' - `hook_namespace` will optionally also replace the binding for namespaced calls.
34+
#' - `pkg_bind()` replaces the binding in the package environment on the search
35+
#' path for unnamespaced calls.
36+
#' - `ns_bind()` replaces the binding for namespaced calls.
3637
#'
3738
#' TODO: Will cause ark to fail to start if `option(defaultPackages = character())`
3839
#' or `R_DEFAULT_PACKAGES=NULL` are set! One idea is to register an `onAttach()`
3940
#' hook here and use that if the package is not loaded yet.
40-
pkg_hook <- function(pkg, name, hook, hook_namespace = NULL) {
41-
env <- sprintf("package:%s", pkg)
42-
env <- as.environment(env)
43-
44-
if (!exists(name, envir = env, mode = "function", inherits = FALSE)) {
45-
msg <- sprintf("Can't register hook: function `%s::%s` not found.", pkg, name)
46-
stop(msg, call. = FALSE)
47-
}
48-
49-
# Replaces the unnamespaced, attached version of the function
50-
hook_original <- env_bind_force(env, name, hook)
51-
52-
# If `hook_namespace` is provided, we try to replace the binding for `pkg::name` as well
53-
if (is.null(hook_namespace)) {
54-
hook_namespace_original <- NULL
55-
} else {
41+
pkg_bind <- function(pkg, name, value) {
42+
env <- sprintf("package:%s", pkg)
43+
env <- as.environment(env)
44+
45+
if (!exists(name, envir = env, mode = "function", inherits = FALSE)) {
46+
msg <- sprintf("Can't register hook: function `%s::%s` not found.", pkg, name)
47+
stop(msg, call. = FALSE)
48+
}
49+
50+
old <- env_bind_force(env, name, value)
51+
invisible(old)
52+
}
53+
54+
ns_bind <- function(pkg, name, value) {
5655
ns <- asNamespace(pkg)
56+
5757
if (!exists(name, envir = ns, mode = "function", inherits = FALSE)) {
58-
msg <- sprintf("Can't replace `%s` in the '%s' namespace.", name, pkg)
59-
stop(msg, call. = FALSE)
58+
msg <- sprintf("Can't replace `%s` in the '%s' namespace.", name, pkg)
59+
stop(msg, call. = FALSE)
6060
}
61-
hook_namespace_original <- env_bind_force(ns, name, hook_namespace)
62-
}
6361

64-
invisible(list(
65-
hook = hook_original,
66-
hook_namespace = hook_namespace_original
67-
))
62+
old <- env_bind_force(ns, name, value)
63+
invisible(old)
6864
}
6965

7066
# R only allows `onLoad` hooks for named packages, not for any package that

crates/ark/src/modules/positron/srcref.R

+39
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,42 @@ reparse_with_srcref <- function(x, name, uri, line) {
4444
zap_srcref <- function(x) {
4545
.ps.Call("ark_zap_srcref", x)
4646
}
47+
48+
new_ark_debug <- function(fn) {
49+
# Signature of `debug()` and `debugonce()`:
50+
# function(fun, text = "", condition = NULL, signature = NULL)
51+
52+
body(fn) <- bquote({
53+
local({
54+
if (!.ps.internal(do_resource_namespaces(default = TRUE))) {
55+
return() # from local()
56+
}
57+
58+
pkgs <- loadedNamespaces()
59+
60+
# Give priority to the namespace of the debugged function
61+
env <- topenv(environment(fun))
62+
if (isNamespace(env)) {
63+
pkgs <- unique(c(getNamespaceName(env), pkgs))
64+
}
65+
66+
# Enable namespace resourcing for all future loaded namespaces and
67+
# resource already loaded namespaces so we get virtual documents for
68+
# step-debugging.
69+
options(ark.resource_namespaces = TRUE)
70+
.ps.internal(resource_namespaces(pkgs))
71+
})
72+
73+
.(body(fn))
74+
})
75+
76+
fn
77+
}
78+
79+
do_resource_namespaces <- function(default) {
80+
getOption("ark.resource_namespaces", default = default)
81+
}
82+
83+
resource_namespaces <- function(pkgs) {
84+
.ps.Call("ps_resource_namespaces", pkgs)
85+
}

crates/ark/src/srcref.rs

+20-9
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,33 @@ use crate::r_task;
1515
use crate::variables::variable::is_binding_fancy;
1616
use crate::variables::variable::plain_binding_force_with_rollback;
1717

18-
#[tracing::instrument(level = "trace")]
19-
pub(crate) fn resource_loaded_namespaces() -> anyhow::Result<()> {
20-
let loaded = RFunction::new("base", "loadedNamespaces").call()?;
21-
let loaded: Vec<String> = loaded.try_into()?;
22-
23-
for pkg in loaded.into_iter() {
24-
r_task::spawn_idle(|| async move {
18+
#[tracing::instrument(level = "trace", skip_all)]
19+
pub(crate) fn resource_namespaces(pkgs: Vec<String>) -> anyhow::Result<()> {
20+
// Generate only one task and loop inside it to preserve the order of `pkgs`
21+
r_task::spawn_idle(|| async move {
22+
for pkg in pkgs.into_iter() {
2523
if let Err(err) = ns_populate_srcref(pkg.clone()).await {
2624
log::error!("Can't populate srcrefs for `{pkg}`: {err:?}");
2725
}
28-
});
29-
}
26+
}
27+
});
3028

3129
Ok(())
3230
}
3331

32+
pub(crate) fn resource_loaded_namespaces() -> anyhow::Result<()> {
33+
let loaded = RFunction::new("base", "loadedNamespaces").call()?;
34+
let loaded: Vec<String> = loaded.try_into()?;
35+
resource_namespaces(loaded)
36+
}
37+
38+
#[harp::register]
39+
unsafe extern "C" fn ps_resource_namespaces(pkgs: SEXP) -> anyhow::Result<SEXP> {
40+
let pkgs: Vec<String> = RObject::view(pkgs).try_into()?;
41+
resource_namespaces(pkgs)?;
42+
Ok(harp::r_null())
43+
}
44+
3445
#[harp::register]
3546
unsafe extern "C" fn ps_ns_populate_srcref(ns_name: SEXP) -> anyhow::Result<SEXP> {
3647
let ns_name: String = RObject::view(ns_name).try_into()?;

0 commit comments

Comments
 (0)