Debugging Display List Internals

by Paul Murrell

cat(format(Sys.time(), "%A %d %B %Y"))

opts_chunk$set(comment=" ", tidy=FALSE) options(width=80)

This report documents the process of debugging a problem with the recording and replaying of R plots from one R session to another. The purpose of this report is to record the source of the problem, to record the solution to the problem, to explain some of the internal details of recorded R plots, and to demonstrate the 'hexView' package for exploring binary blobs.

The problem

The recordPlot() function allows a "snapshot" of the current R plot to be recorded as an R object (a "recordedplot"), and the replayPlot() function can be used to redraw a "recordedplot". In the development version of R (to become R 3.3.0), a "recordedplot" can be saved to disk with saveRDS() and then reloaded in a different R session with readRDS(). This should work between R sessions on different platforms (e.g., record on Linux and replay on Windows).

Henrik Bengtsson discovered that, if we record a plot on Linux using the PNG device, it does not replay correctly on Windows (the replayed plot is blank). He provided example "recordedplot"s from Linux and from Windows to demonstrate the problem. The "recordedplot" created on Windows replayed correctly on both Linux and Windows, the "recordedplot" created on Linux replayed correctly on Linux, but not on Windows.

Each "recordedplot" was created with something like the following R code ...

png("dummy.png") dev.control("enable") plot(1:10) rp <- recordPlot() saveRDS(rp, "R-recordplot_LinuxA.rds")

... and then replayed with ...

rp <- readRDS("R-recordplot_LinuxA.rds") replayPlot(rp)

Debugging the problem

The debugging effort focused on looking for differences between the "recordedplot" created on Windows and the "recordedplot" created on Linux (because the former replayed fine and the latter did not).

Each "recordedplot" is a list of two components: the first is a set calls to internal C graphics functions ...

rp <- readRDS("R-recordplot_LinuxA.rds") rp[[1]][[1]]

... and the second is state information for the 'graphics' package.

head(rp[[2]], 100)

The following function was written to capture the printed display of a "recordedplot" to a text file ...

printRDS <- function(infile, outfile) { x <- readRDS(infile) displaylist <- capture.output(print(x[[1]])) graphicsContext <- capture.output(print(x[[2]])) writeLines(c(displaylist, graphicsContext), outfile) }

... and this function was used to create two text files that could be 'diff'ed ...

printRDS("R-recordplot_Windows.rds", "printRDS_Windows.txt") printRDS("R-recordplot_LinuxA.rds", "printRDS_LinuxA.txt") diff <- system("diff printRDS_Windows.txt printRDS_LinuxA.txt", intern=TRUE)

This revealed two sorts of differences. The first is not at all surprising: the information on internal C calls has paths to DLLs on Windows and paths to shared object (.so) files on Linux, for example, ...

diff[1:5]

This difference is not a concern because these paths are rebuilt when a "recordedplot" is loaded into a new R session.

However, the second difference is more of a concern: there were differences in the 'graphics' state information, for example ...

diff[41:44]

These differences required further exploration, but to do that we need a better view of the state information. We can see, if we look closely, that there are two bytes different in the example above, but it is not very clear what that two-byte difference represents. This is where the 'hexView' package comes in.

The following function was used to create a binary file that just contained the 'graphics' state information ...

exportGraphicsContext <- function(infile, outfile) { x <- readRDS(infile) graphicsContext <- x[[2]] # as.vector() to strip attributes writeBin(as.vector(graphicsContext), outfile) } exportGraphicsContext("R-recordplot_Windows.rds", "graphicsContext_Windows.bin") exportGraphicsContext("R-recordplot_LinuxA.rds", "graphicsContext_LinuxA.bin")

Now we can use the 'hexView' package to view the contents of the binary files ...

library(hexView) viewRaw("graphicsContext_Windows.bin", nbytes=100)

That is not a huge improvement because it is interpreting each byte in the file as an ASCII character. We can do better if we tell 'hexView' how to interpret sequences of bytes within the file. To do that, we need a description of the 'graphics' state information data structure, which we can get from the "Graphics.h" file in R's source code (the Resources Section has a link to the online subversion repository). A small snippet of that information is shown below.

   typedef struct {
    /* Plot State */
    /*
       When the device driver is started this is 0
       After the first call to plot.new/perps it is 1
       Every graphics operation except plot.new/persp
       should fail if state = 0
       This is checked at the highest internal function
       level (e.g., do_lines, do_axis, do_plot_xy, ...)
    */

    int	state;		/* plot state: 1 if GNewPlot has been called
			   (by plot.new or persp) */
    Rboolean valid;	/* valid layout ?  Used in GCheckState & do_playDL */

    /* GRZ-like Graphics Parameters */
    /* ``The horror, the horror ... '' */
    /* Marlon Brando - Appocalypse Now */

    /* General Parameters -- set and interrogated directly */

    double adj;		/* String adjustment */ 
  

Ignoring the colourful comments, we have the following information: the state information starts with a 4-byte integer, followed by an "Rboolean" (another 4-byte integer), followed by an 8-byte double.

We can describe that structure to 'hexView' as follows and the result is much easier to interpret (the integer zero, twice, then the numeric value 0.5) ...

GPar <- memFormat(state=integer4, valid=integer4, adj=real8) viewFormat("graphicsContext_Windows.bin", GPar)

We can continue that process to create a complete description of the 'graphics' state information (the Resources Section has a link to complete code). The following shows just the part where we have seen a difference.

GParFragment <- memFormat(font=integer4, gamma=real8, lab=vectorBlock(integer4, 3)) viewFormat("graphicsContext_Windows.bin", GParFragment, offset=292)

The same view of the "recordedplot" from Linux shows that the difference is in the value of 'gamma', which is 1 on Windows and 0 on Linux.

GParFragment <- memFormat(font=integer4, gamma=real8, lab=vectorBlock(integer4, 3)) viewFormat("graphicsContext_LinuxA.bin", GParFragment, offset=292)

This discovery lead to the diagnosis that the Cairo graphics device on Linux was not initialising its 'startgamma' value, so it was defaulting to zero, and when the Windows graphics device applied that zero gamma value all of the drawing colours were converted to white. So when replaying a "recordedplot" on a Windows graphics device, when the "recordedplot" had been created on a Cairo graphics device on Linux, the replayed plot was drawn white-on-white (which appears blank).

This problem was fixed in commit r70080 to the R subversion repository.

Data structure alignment

One interesting wrinkle arose when attempting to describe the C code description of the 'graphics' state information to 'hexView'. Consider the following excerpt from "Graphics.h" ...

    double adj;		/* String adjustment */
    Rboolean ann;	/* Should annotation take place */
    rcolor bg;		/* **R ONLY** Background color */
    char bty;		/* Box type */
    double cex;		/* Character expansion */
    double lheight;     /* Line height
  

If we translate that naively, we get: 8-byte real, 4-byte integer, a sequence of four 1-byte unsigned integers, 1-byte character, 8-byte real, and an 8-byte real. However, that clearly is not right (both 'cex' and 'lheight' should be 1) ...

rcolor <- vectorBlock(atomicBlock("int", size=1, signed=FALSE), 4) GParFragment <- memFormat(adj=real8, ann=integer4, bg=rcolor, bty=ASCIIchar, cex=real8, lheight=real8) viewFormat("graphicsContext_Windows.bin", GParFragment, offset=8)

The issue here is that the 8-byte real for 'cex' has to start in memory on a multiple of 8-bytes (25 is not a multiple of 8). In order for that to happen, the data structure is "padded" with extra bytes. So the memory actually looks like this (32 is a multiple of 8) ...

GParFragment <- memFormat(adj=real8, ann=integer4, bg=rcolor, bty=ASCIIchar, padding=memBlock(7), cex=real8, lheight=real8) viewFormat("graphicsContext_Windows.bin", GParFragment, offset=8)

The rules for padding data structures may be sensitive to both hardware and software platforms, so this may be a foreshadowing of future problems with moving a "recordedplot" between 32-bit and 64-bit systems and/or between i386 and other chip sets.

Acknowledgements

Thanks to Henrik Bengtsson for reporting the problem, for providing nice "recordedplot"s to assist with the diagnosis, and for helping to confirm that the fix works.

Resources

This report was generated on Ubuntu 14.04 64-bit running R Under development (unstable) (2016-02-03 r70080) with version 0.3-4 of the 'hexView' package.


Creative Commons License
Debugging Display List Internals by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.