Skip to content

Commit 527835a

Browse files
Detect and alert on default graphics device being used
1 parent bf3f271 commit 527835a

File tree

9 files changed

+154
-3
lines changed

9 files changed

+154
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Package: future
2-
Version: 1.49.0-9014
2+
Version: 1.49.0-9015
33
Title: Unified Parallel and Distributed Processing in R for Everyone
44
Depends:
55
R (>= 3.2.0)

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ export(ClusterFutureBackend)
127127
export(ConnectionMisuseFutureCondition)
128128
export(ConnectionMisuseFutureError)
129129
export(ConnectionMisuseFutureWarning)
130+
export(DefaultDeviceMisuseFutureCondition)
131+
export(DefaultDeviceMisuseFutureError)
132+
export(DefaultDeviceMisuseFutureWarning)
130133
export(DeviceMisuseFutureCondition)
131134
export(DeviceMisuseFutureError)
132135
export(DeviceMisuseFutureWarning)

NEWS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Version (development version)
22

3+
## New Features
4+
5+
* Now futures produce a warning when they detect that the _default_
6+
graphics device, as defined by R option `"device"`, is opened by,
7+
for instance, a `plot()` call without explicitly opening a graphics
8+
device. The reason for this check is that we rarely want to plot to
9+
the _default_ graphics device in parallel processing, which
10+
typically ends up plotting to a `Rplots.pdf` file that is local to
11+
the parallel worker. If that is truly wanted, please open a
12+
graphics devices explicitly (e.g. `pdf()` or `png()`) before
13+
plotting. Alternatively, explicitly set R option `device` inside
14+
the future expression.
15+
316
## Bug Fixes
417

518
* The `multicore` backend did not relay `immediateCondition`:s in a

R/backend_api-evalFuture.R

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,7 @@ evalFutureInternal <- function(data) {
914914

915915

916916
checkDevices <- getOption("future.devices.onMisuse")
917+
...future.option.defaultDevice <- 0L
917918
if (is.null(checkDevices)) {
918919
checkDevices <- TRUE
919920
} else {
@@ -922,6 +923,25 @@ evalFutureInternal <- function(data) {
922923
if (checkDevices) {
923924
## IMPORTANT: Need to use as.list() - if not, it's a reference variable/alias (sic!)
924925
...future.devices <- as.list(base::.Devices)
926+
## Detect attempts to open the default graphics device
927+
device <- getOption("device")
928+
if (!is.null(device)) {
929+
if (is.character(device)) {
930+
if (exists(device, mode = "function")) {
931+
device <- get(device, mode = "function")
932+
} else {
933+
device <- NULL
934+
}
935+
}
936+
if (!is.null(device)) {
937+
...future.option.device <- device
938+
options(device = function(...) {
939+
...future.option.defaultDevice <<- ...future.option.defaultDevice + 1L
940+
## Call the default graphics device
941+
...future.option.device(...)
942+
})
943+
}
944+
}
925945
}
926946

927947

@@ -949,6 +969,7 @@ evalFutureInternal <- function(data) {
949969
misuseGlobalEnv = if (checkGlobalenv) list(added = diff_globalenv(...future.globalenv.names)) else NULL,
950970
misuseConnections = if (checkConnections) diff_connections(get_connections(details = isTRUE(attr(checkConnections, "details", exact = TRUE))), ...future.connections) else NULL,
951971
misuseDevices = if (checkDevices) diff_devices(...future.devices, base::.Devices) else NULL,
972+
misuseDefaultDevice = ...future.option.defaultDevice,
952973
started = ...future.startTime
953974
)
954975
}, condition = function(cond) {
@@ -1049,6 +1070,7 @@ evalFutureInternal <- function(data) {
10491070
misuseGlobalEnv = if (checkGlobalenv) list(added = diff_globalenv(...future.globalenv.names)) else NULL,
10501071
misuseConnections = diff_connections(get_connections(details = isTRUE(attr(checkConnections, "details", exact = TRUE))), ...future.connections),
10511072
misuseDevices = if (checkDevices) diff_devices(base::.Devices, ...future.devices) else NULL,
1073+
misuseDefaultDevice = ...future.option.defaultDevice,
10521074
started = ...future.startTime
10531075
)
10541076
}, error = function(ex) {
@@ -1059,6 +1081,7 @@ evalFutureInternal <- function(data) {
10591081
misuseGlobalEnv = if (checkGlobalenv) list(added = diff_globalenv(...future.globalenv.names)) else NULL,
10601082
misuseConnections = diff_connections(get_connections(details = isTRUE(attr(checkConnections, "details", exact = TRUE))), ...future.connections),
10611083
misuseDevices = if (checkDevices) diff_devices(base::.Devices, ...future.devices) else NULL,
1084+
misuseDefaultDevice = ...future.option.defaultDevice,
10621085
started = ...future.startTime
10631086
)
10641087
}) ## output tryCatch()

R/core_api-value.R

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,44 @@ value.Future <- function(future, stdout = TRUE, signal = TRUE, drop = FALSE, ...
239239
}
240240
}
241241

242+
## ------------------------------------------------------------------
243+
## Report on misuse of the default devices
244+
## ------------------------------------------------------------------
245+
if (!is.null(result[["misuseDefaultDevice"]]) &&
246+
result[["misuseDefaultDevice"]] > 0L) {
247+
onMisuse <- getOption("future.defaultDevice.onMisuse")
248+
if (is.null(onMisuse)) onMisuse <- "warning"
249+
if (onMisuse != "ignore") {
250+
if (onMisuse == "error") {
251+
cond <- DefaultDeviceMisuseFutureError(times = result[["misuseDefaultDevice"]], future = future)
252+
} else if (onMisuse == "warning") {
253+
cond <- DefaultDeviceMisuseFutureWarning(times = result[["misuseDefaultDevice"]], future = future)
254+
} else {
255+
cond <- NULL
256+
warnf("Unknown value on option 'future.defaultDevice.onMisuse': %s",
257+
sQuote(onMisuse))
258+
}
259+
260+
if (!is.null(cond)) {
261+
## FutureCondition to stack of captured conditions
262+
new <- list(condition = cond, signaled = FALSE)
263+
conditions <- result[["conditions"]]
264+
n <- length(conditions)
265+
266+
## An existing run-time error takes precedence
267+
if (n > 0L && inherits(conditions[[n]][["condition"]], "error")) {
268+
conditions[[n + 1L]] <- conditions[[n]]
269+
conditions[[n]] <- new
270+
} else {
271+
conditions[[n + 1L]] <- new
272+
}
273+
274+
result[["conditions"]] <- conditions
275+
future[["result"]] <- result
276+
}
277+
}
278+
}
279+
242280

243281
## ------------------------------------------------------------------
244282
## Report on misuse of the RNG

R/protected_api-FutureCondition-class.R

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ GlobalEnvMisuseFutureCondition <- function(message = NULL, call = NULL, differen
194194
if (is.null(message)) {
195195
label <- sQuoteLabel(future[["label"]])
196196
message <- sprintf("%s (%s) added variables to the global environment. A future expression should never assign variables to the global environment - neither by assign() nor by <<-: [n=%d] %s", class(future)[1], label, length(differences[["added"]]), commaq(differences[["added"]]))
197+
message <- sprintf("%s. See also help(\"future.options\", package = \"future\")", message)
197198
}
198199
cond <- FutureCondition(message = message, call = call, uuid = uuid, future = future)
199200
cond[["differences"]] <- differences
@@ -244,6 +245,7 @@ ConnectionMisuseFutureCondition <- function(message = NULL, call = NULL, differe
244245
details <- paste(details, collapse = ", ")
245246
message <- sprintf("%s. Details: %s", message, details)
246247
}
248+
message <- sprintf("%s. See also help(\"future.options\", package = \"future\")", message)
247249
}
248250
cond <- FutureCondition(message = message, call = call, uuid = uuid, future = future)
249251
cond[["differences"]] <- differences
@@ -288,6 +290,7 @@ DeviceMisuseFutureCondition <- function(message = NULL, call = NULL, differences
288290
details <- sprintf("%d devices differ: %s", length(details), paste(details, collapse = "; "))
289291
message <- sprintf("%s. Details: %s", message, details)
290292
}
293+
message <- sprintf("%s. See also help(\"future.options\", package = \"future\")", message)
291294
}
292295
cond <- FutureCondition(message = message, call = call, uuid = uuid, future = future)
293296
cond[["differences"]] <- differences
@@ -316,6 +319,40 @@ DeviceMisuseFutureError <- function(...) {
316319

317320

318321

322+
#' @rdname FutureCondition
323+
#' @export
324+
DefaultDeviceMisuseFutureCondition <- function(message = NULL, times = 0L, call = NULL, uuid = future[["uuid"]], future = NULL) {
325+
if (is.null(message)) {
326+
label <- sQuoteLabel(future[["label"]])
327+
message <- sprintf("%s (%s) opened the default graphics device %d times. This happens for instance if plot() is called without explicitly opening a graphics device before. Using default graphics devices in parallel processing will typically leave behind a 'Rplots.pdf' file on the parallel worker. If the intention is to plot to file, please open a graphics device explicitly (e.g. pdf() or png()), the plot, and make sure to close it at the end (i.e. dev.off())", class(future)[1], label, times)
328+
message <- sprintf("%s. See also help(\"future.options\", package = \"future\")", message)
329+
}
330+
cond <- FutureCondition(message = message, call = call, uuid = uuid, future = future)
331+
class <- c("DefaultDeviceMisuseFutureCondition", "MisuseFutureCondition", class(cond))
332+
class(cond) <- class[!duplicated(class, fromLast = TRUE)]
333+
cond
334+
}
335+
336+
#' @rdname FutureCondition
337+
#' @export
338+
DefaultDeviceMisuseFutureWarning <- function(...) {
339+
cond <- DefaultDeviceMisuseFutureCondition(...)
340+
class <- c("DefaultDeviceMisuseFutureWarning", "MisuseFutureWarning", "FutureWarning", "warning", class(cond))
341+
class(cond) <- class[!duplicated(class, fromLast = TRUE)]
342+
cond
343+
}
344+
345+
#' @rdname FutureCondition
346+
#' @export
347+
DefaultDeviceMisuseFutureError <- function(...) {
348+
cond <- DefaultDeviceMisuseFutureCondition(...)
349+
class <- c("DefaultDeviceMisuseFutureError", "MisuseFutureError", "FutureError", "error", class(cond))
350+
class(cond) <- class[!duplicated(class, fromLast = TRUE)]
351+
cond
352+
}
353+
354+
355+
319356
#' @rdname FutureCondition
320357
#' @export
321358
FutureLaunchError <- function(..., future = NULL) {

R/utils-options.R

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@
9494
#' (Default: `"warning"`)
9595
#' }
9696
#'
97+
#' \item{\option{future.defaultDevice.onMisuse}:}{(character string)
98+
#' A future must open graphics devices explicitly, if it creates new
99+
#' plots. It should not rely on the default graphics device that
100+
#' is given by R option `"default"`, because that rarely does what
101+
#' is intended.
102+
#' If such misuse is detected and this option is set to `"error"`,
103+
#' then an informative error is produced. If it is set to `"warning"`,
104+
#' a warning is produced. If`"ignore"`, no check is performed.
105+
#' (Default: `"warning"`)
106+
#' }
107+
#'
97108
#' \item{\option{future.devices.onMisuse}:}{(character string)
98109
#' A future must close any graphics devices it opens and must not close
99110
#' devices it did not open itself.
@@ -111,7 +122,7 @@
111122
#' when variables are added to the global environment.
112123
#' If this is detected, and this option is set to `"error"`, then an
113124
#' informative error is produced. If `"warning"`, then a warning is
114-
#' iproduced. If `"ignore"`, no check is performed.
125+
#' produced. If `"ignore"`, no check is performed.
115126
#' (Default: `"ignore"`)
116127
#' }
117128
#'

man/FutureCondition.Rd

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/zzz-future.options.Rd

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)