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

Move all plot watching logic into watchout #151

Merged
merged 11 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# evaluate (development version)

* `watchout()` is no longer exported; it's really an implementation detail that should never have been leaked to the public interface.
* `evaluate()` gains an output class (`evaluate_evaluation`/`list`) and a basic print method.
* `evaluate()` now correctly captures plots created before messages/warnings/errors (#28).

Expand Down
35 changes: 7 additions & 28 deletions R/eval.R
Original file line number Diff line number Diff line change
Expand Up @@ -69,40 +69,18 @@ evaluate <- function(input,
}
local_inject_funs(envir)

if (new_device) {
# Ensure we have a graphics device available for recording, but choose
# one that's available on all platforms and doesn't write to disk
pdf(file = NULL)
dev.control(displaylist = "enable")
dev <- dev.cur()
on.exit(dev.off(dev))
}
# record the list of current devices
devs <- .env$dev_list; on.exit(.env$dev_list <- devs, add = TRUE)
devn <- length(.env$dev_list <- dev.list())
dev <- dev.cur()

# clean up the last_plot object after an evaluate() call (cf yihui/knitr#722)
on.exit(assign("last_plot", NULL, envir = environment(plot_snapshot)), add = TRUE)

# if this env var is set to true, always bypass messages
if (tolower(Sys.getenv('R_EVALUATE_BYPASS_MESSAGES')) == 'true')
keep_message = keep_warning = NA

# Capture output
watcher <- watchout(output_handler, debug = debug)
watcher <- watchout(output_handler, new_device = new_device, debug = debug)

out <- vector("list", nrow(parsed))
for (i in seq_along(out)) {
if (debug) {
message(parsed$src[[i]])
}

# if dev.off() was called, make sure to restore device to the one opened by
# evaluate() or existed before evaluate()
if (length(dev.list()) < devn) dev.set(dev)
devn <- length(dev.list())

out[[i]] <- evaluate_top_level_expression(
exprs = parsed$expr[[i]],
src = parsed$src[[i]],
Expand All @@ -117,6 +95,7 @@ evaluate <- function(input,
output_handler = output_handler,
include_timing = include_timing
)
watcher$check_devices()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just curious: what does motivate the move for this refactored code after evaluate_top_level_expression ?

All tests are passing so seems good - but wanted to understand the thinking as I would have place it in the same place by precaution

Copy link
Member Author

@hadley hadley Jun 19, 2024

Choose a reason for hiding this comment

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

Over time, my plan is to move to more and more logic in to evaluate() so the logic is more clear. In this case specifically, I think it makes sense to check the devices after you've run the user code, rather than before. (Although the net effect is generally going to be the same.)


if (stop_on_error > 0L) {
errs <- vapply(out[[i]], is.error, logical(1))
Expand Down Expand Up @@ -154,12 +133,12 @@ evaluate_top_level_expression <- function(exprs,
source <- new_source(src, exprs[[1]], output_handler$source)
output <- list(source)

dev <- dev.cur()
handle_output <- function(plot = TRUE, incomplete_plots = FALSE) {
# if dev.cur() has changed, we should not record plots any more
plot <- plot && identical(dev, dev.cur())
out <- watcher(plot, incomplete_plots)
output <<- c(output, out)
out <- list(
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't like this interface so it's likely to change in a future PR.

if (plot) watcher$capture_plot(incomplete_plots),
watcher$capture_output()
)
output <<- c(output, compact(out))
}

local_output_handler(function() handle_output(FALSE))
Expand Down
42 changes: 0 additions & 42 deletions R/graphics.R
Original file line number Diff line number Diff line change
@@ -1,45 +1,3 @@
#" Capture snapshot of current device.
#"
#" There's currently no way to capture when a graphics device changes,
#" except to check its contents after the evaluation of every expression.
#" This means that only the last plot of a series will be captured.
#"
#" @return \code{NULL} if plot is blank or unchanged, otherwise the output of
#" \code{\link{recordPlot}}.
plot_snapshot <- local({
last_plot <- NULL

function(incomplete = FALSE) {
devs <- dev.list()
# No graphics devices
if (is.null(devs)) {
return()
}

# Current graphics device changed since evaluate started
if (!identical(devs, .env$dev_list)) {
return()
}

# current page is incomplete
if (!par("page") && !incomplete) {
return()
}

plot <- recordPlot()
if (!makes_visual_change(plot[[1]])) {
return()
}

if (!looks_different(last_plot[[1]], plot[[1]])) {
return()
}

last_plot <<- plot
plot
}
})

looks_different <- function(old_dl, new_dl) {
if (identical(old_dl, new_dl)) {
return(FALSE)
Expand Down
82 changes: 62 additions & 20 deletions R/watcher.R
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
#' Watch for changes in output, text and graphics
#'
#' @param debug activate debug mode where output will be both printed to
#' screen and captured.
#' @param handler An ouptut handler object.
#' @param frame When this frame terminates, the watcher will automatically close.`
#' @return list containing four functions: `get_new`, `pause`,
#' `unpause`, `close`.
#' @keywords internal
watchout <- function(handler = new_output_handler(),
new_device = TRUE,
debug = FALSE,
frame = parent.frame()) {
last_plot <- NULL

if (new_device) {
# Ensure we have a graphics device available for recording, but choose
# one that's available on all platforms and doesn't write to disk
pdf(file = NULL)
dev.control(displaylist = "enable")
dev <- dev.cur()
defer(dev.off(dev), frame)
}

# record current devices
devs <- dev.list()
devn <- length(devs)
dev <- dev.cur()

con <- file("", "w+b")
defer(frame = frame, {
if (!test_con(con, isOpen)) {
Expand All @@ -24,20 +32,54 @@ watchout <- function(handler = new_output_handler(),
old <- options(try.outFile = con)
defer(options(old), frame = frame)

function(plot = TRUE, incomplete_plots = FALSE) {
out <- list(
if (plot) plot_snapshot(incomplete_plots),
read_con(con)
)
if (!is.null(out[[1]])) {
handler$graphics(out[[1]])
capture_plot <- function(incomplete = FALSE) {
# only record plots for our graphics device
if (!identical(dev.cur(), dev)) {
return()
}

# current page is incomplete
if (!par("page") && !incomplete) {
return()
}

plot <- recordPlot()
if (!makes_visual_change(plot[[1]])) {
return()
}
if (!is.null(out[[2]])) {
handler$text(out[[2]])

if (!looks_different(last_plot[[1]], plot[[1]])) {
return()
}

last_plot <<- plot
handler$graphics(plot)
plot
}

capture_output <- function() {
out <- read_con(con)
if (!is.null(out)) {
handler$text(out)
}
out
}

check_devices <- function() {
# if dev.off() was called, make sure to restore device to the one opened
# when watchout() was called
if (length(dev.list()) < devn) {
dev.set(dev)
}

compact(out)
devn <<- length(dev.list())
invisible()
}

list(
capture_plot = capture_plot,
capture_output = capture_output,
check_devices = check_devices
)
}

read_con <- function(con, buffer = 32 * 1024) {
Expand Down
24 changes: 0 additions & 24 deletions man/watchout.Rd

This file was deleted.

6 changes: 4 additions & 2 deletions tests/testthat/test-graphics.R
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ test_that("perspective plots are captured", {
})

# a bug report yihui/knitr#722
test_that("repeatedly drawing the same plot does not omit plots randomly", {
expect_true(all(replicate(100, length(evaluate("plot(1:10)"))) == 2))
test_that("plot state doesn't persist over evaluate calls", {
expect_output_types(evaluate("plot(1)"), c("source", "plot"))
expect_output_types(evaluate("plot(1)"), c("source", "plot"))
expect_output_types(evaluate("plot(1)"), c("source", "plot"))
})

test_that("evaluate() doesn't depend on device option", {
Expand Down
Loading