Why do some function calls not show in the profiler?
As noted earlier, some of R’s built-in functions don’t show in the
profvis flame graph. These include functions like <-
,
[
, and $
. Although these functions can occupy
a lot of time, they don’t show on the call stack. (In one of the
examples above, $
does show on the call stack, but this is
because it was dispatched to $.data.frame
, as opposed to
R’s internal C code, which is used for indexing into lists.)
In some cases the side-effects of these functions can be seen in the
flamegraph. As we saw in the example above, using these functions in a
loop led to many memory allocations, which led to garbage collections,
or <GC>
blocks in the flame graph.
How do I share a profvis visualization?
Right now the easiest way to do this is to run profvis
in RStudio, and publish to RPubs.
Once the profile shows up in the RStudio IDE, click on the Publish button to send it to RPubs. You can see an example here. If you don’t already have an account on RPubs, you’ll need to set one up.
You can also click on the save (disk) icon. This will save the profvis visualization to an .Rprofvis file. This file can be opened by RStudio, or if you rename it to have an .html extension, it can be opened in a web browser.
Another way to publish a profvis visualization is to save the HTML
output file using htmlwidgets::saveWidget
, and put that on
any web hosting service:
p <- profvis({
# Interesting code here
})
htmlwidgets::saveWidget(p, "profile.html")
It’s also possible to put a profvis visualization in a knitr document. At this time, some CSS workarounds are needed needed for them to display properly. You can look at the source of this website to see the workarounds.
What does <Anonymous>
mean?
It’s not uncommon for R code to contain anonymous functions
– that is, functions that aren’t named. These show up as
<Anonymous>
in the profiling data collected from
Rprof
.
In the code below there is a function, make_adder
, that
returns a function. We’ll invoke the returned function in two ways.
First, we’ll run make_adder(1)(10)
. The call
make_adder(1)
returns a function, which is invoked
immediately (without being saved in a variable), and shows up as
<Anonymous>
in the flame graph.
Next, we’ll call make_adder(2)
but this time, we’ll save
the result in a variable, adder2
. Then we’ll call
adder2(10)
. When we do it this way, the profiler records
that the function label is adder2
.
profvis({
make_adder <- function(n) {
function(x) {
pause(0.25) # Wait for a moment so that this shows in the profiler
x + n
}
}
# Called with no intermediate variable, it shows as "<Anonymous>"
make_adder(1)(10)
# With the function saved in a variable, it shows as "adder2"
adder2 <- make_adder(2)
adder2(10)
})
Those are equivalent to `::`(package, function)
and
`$`(x, "fun")
, respectively. These calls return anonymous
functions, and so R’s internal profiling code labels these as
<Anonymous>
. If you want labels in the profiler to
have a different label, you can assign the value to a temporary variable
(like adder2
above), and then invoke that.
Finally, if a function is passed to lapply
, it will be
show up as FUN
in the flame graph. If we inspect the source
code for lapply
, it’s clear why: when a function is passed
to lapply
, the name used for the function inside of
lapply
is FUN
.
lapply
#> function (X, FUN, ...)
#> {
#> FUN <- match.fun(FUN)
#> if (!is.vector(X) || is.object(X))
#> X <- as.list(X)
#> .Internal(lapply(X, FUN))
#> }
#> <bytecode: 0x5571d0708c50>
#> <environment: namespace:base>
What does cmpfun
mean?
The first time we run profvis
on a function in a clean
3.4.0 or greater R session, we’ll see compiler:::tryCmpfun
.
For example,
profvis({
data <- data.frame(value = runif(5e4))
csum <- function(x) {
if (length(x) < 2) return(x)
sum <- x[1]
for (i in seq(2, length(x))) {
sum[i] <- sum[i-1] + x[i]
}
sum
}
data$sum <- csum(data$value)
})
R attempts to compile functions when they are first ran to byte code.
On subsequent function calls, instead of reinterpreting the body of the
function, R executes the saved and compiled byte code. Typically, this
results in faster execution times on later function calls. For example,
let’s profile csum
a second time in the same R session:
Now the flame graph shows that the function is no longer being
compiled. And after compiling, csum
is about 40 ms
faster.
How do I get code from an R package to show in the code panel?
In typical use, only code written by the user is shown in the code panel. (This is code for which source references are available.) Yellow blocks in the flame graph have corresponding lines of code in the code panel, and when moused over, the line of code will be highlighted. White blocks in the flame graph don’t have corresponding lines in the code panel. In most cases, the calls represented by the white blocks are to functions that are in base R and other packages.
profvis can also show code that’s inside an R package. To do this,
source refs for the package code must be available. There are two
general ways to do this: you can install the package with source refs,
or you can use devtools::load_all()
to load a package from
sources on disk.
Installing with source refs
There are many ways to install a package with source refs. Here are some examples of installing ggplot2:
-
From CRAN:
## First, restart R ## install.packages("ggplot2", type="source", INSTALL_opts="--with-keep.source")
From an RStudio package project on local disk: Go to Build -> Configure Build Tools -> Build Tools -> Build and Reload – R CMD INSTALL additional options, and add
--with-keep.source
. Then run Build -> Build and Reload.-
From sources on disk with devtools:
## First, restart R ## # Assuming sources are in a subdirectory ggplot2/ devtools::install("ggplot2", keep_source = TRUE)
-
From sources on disk using the command line:
-
From sources on Github:
## First, restart R ## remotes::install_github("hadley/ggplot2", INSTALL_opts="--with-keep.source")
Loading packages with source refs (without installing)
-
Instead of installing an in-development package, you can simply load it from source using devtools.
# Assuming sources are in a subdirectory ggplot2/ devtools::load_all("ggplot2")
Once a package is loaded or installed with source refs, profvis visualizations will display source code for that package. For example, the visualization below has yellow blocks for both user code and for code in ggplot2, and it contains ggplot2 code in the code panel:
Can I profile code without calling profvis()
?
Yes. There are two ways to do it.
If you are in RStudio, you can select Profile->Start Profiling, run your code, and then Profile->Stop Profiling. When you stop the profiling, the profvis viewer will come up.
Another way is to start and stop the R profiler manually, then have profvis read in the recorded profiling data. To profile your code, run:
# Start profiler
Rprof("data.Rprof", interval = 0.01, line.profiling = TRUE,
gc.profiling = TRUE, memory.profiling = TRUE)
## Run your code here
# Stop profiler
Rprof(NULL)
Then you can load the data into profvis:
profvis(prof_input = "data.Rprof")
This technique can also be used to profile just one section of your code.
Why does the flame graph hide some function calls for Shiny apps?
When profiling Shiny applications, the profvis flame graph will hide many function calls by default. They’re hidden because they aren’t particularly informative for optimizing code, and they add visual complexity. This feature requires Shiny 0.13.0 or greater.
If you want to see these hidden blocks, uncheck Options -> Hide internal function calls:
library(shiny)
profvis({
# After this app has started, interact with it a bit, then quit
runExample("06_tabsets", display.mode = "normal")
})
To make the hiding work, Shiny has special functions called
..stacktraceon..
and ..stacktraceoff..
.
profvis goes up the stack, and when it sees a
..stacktraceoff..
, it will hide all function calls until it
sees a corresponding ..stacktraceon..
. If there are
nukltiple ..stacktraceoff..
calls in the stack, it requires
an equal number of ..stacktraceon..
calls before it starts
displaying function calls again.
Can I profile just part of a Shiny application?
Sometimes it it useful to profile just part of a Shiny application, instead of the whole thing from start to finish.
If you are in RStudio, you can start your application, then select Profile->Start Profiling, interact with your application, and then select Profile->Stop Profiling. When you stop the profiling, the profvis viewer will come up.
Profivs also provides a Shiny Module
to initiate the profiling, and provides a UI to start, stop, view, and
download profvis sessions. This is done with
profvis::profvis_server
and
profvis::profvis_ui
.
For example, here’s a small app that uses the module:
library(shiny)
library(ggplot2)
library(profvis)
shinyApp(
fluidPage(
plotOutput("plot"),
actionButton("new", "New plot"),
profvis_ui("profiler")
),
function(input, output, session) {
callModule(profvis_server, "profiler")
output$plot <- renderPlot({
input$new
ggplot(diamonds, aes(carat, price)) + geom_point()
})
}
)
In the server function,
callModule(profvis_server, "profiler")
sets up the profvis
session, and in the UI profvis_ui("profiler")
sets up a
basic interface to start, stop, view, and download profvis sessions.
You can create your own profvis_server
and
profvis_ui
functions by calling Rprof()
to
start and stop profiling (as described in this answer), and
trigger it with an actionButton
. For example, you could put
this in your UI:
radioButtons("profile", "Profiling", c("off", "on"))
And put this in your server function:
observe({
if (identical(input$profile, "off")) {
Rprof(NULL)
} else if (identical(input$profile, "on")){
Rprof(strftime(Sys.time(), "%Y-%m-%d-%H-%M-%S.Rprof"),
interval = 0.01, line.profiling = TRUE,
gc.profiling = TRUE, memory.profiling = TRUE)
}
})
It will add radio buttons to turn profiling on and off. Turn it on, then interact with your app, then turn it off. There will be a file with a name corresponding to the start time. You can view the profiler output with profvis, with something like this:
profvis(prof_input = "2018-08-07-12-22-35.Rprof")
Can I profile a Shiny application running on a remote server?
Yes. One option is to include the profvis Shiny Module desribed in the previous question.
You can also set it up manually. The main idea is to start and stop profiling (as described in this answer). At the top of your app.R or server.R, you can add the following:
Rprof(strftime(Sys.time(), "%Y-%m-%d-%H-%M-%S.Rprof"),
interval = 0.01, line.profiling = TRUE,
gc.profiling = TRUE, memory.profiling = TRUE)
onStop(function() {
Rprof(NULL)
})
This will start profiling when the app starts, and stop when it exits.
Why does Sys.sleep()
not show up in profiler data?
The R profiler doesn’t provide any data when R makes a system call.
If, for example, you call Sys.sleep(5)
, the R process will
pause for 5 seconds, but you probably won’t see any instances of
Sys.sleep
in the profvis visualization – it won’t even take
any horizontal space. For these examples, we’ve used the
pause
function instead, which is part of the profvis
package. It’s similar to Sys.sleep
, except that it does
show up in the profiling data. For example:
profvis({
# Does not show in the flame graph
Sys.sleep(0.25)
# Does show in the flame graph
pause(0.25)
})
Calls to external programs and libraries also may not show up in the profiling data. If you call functions from a package to fetch data from external sources, keep in mind that time spent in those functions may not show in the profiler.
Why is the call stack reversed for some of my code?
One of the unusual features of R as a programming language is that it has lazy evaluation of function arguments. If you pass an expression to a function, that expression won’t be evaluated until it’s actually used somewhere in that function.
The result of this is that sometimes the stack can look like it’s in
the wrong order. In this example below, we call times_10
and times_10_lazy
. They both call times_5()
and times_2()
, but the “regular” version uses an
intermediate variable y
, while the lazy version nests the
calls, with times_2(times_5(x))
.
profvis({
times_5 <- function(x) {
pause(0.5)
x * 5
}
times_2 <- function(x) {
pause(0.2)
x * 2
}
times_10 <- function(x) {
y <- times_5(x)
times_2(y)
}
times_10_lazy <- function(x) {
times_2(times_5(x))
}
times_10(10)
times_10_lazy(10)
})
In most programming languages, the flame graph would look the same
for both: the times_10
(or times_10_lazy
)
block would be on the bottom, with times_5
and
times_2
side-by-side on the next level up on the stack.
With lazy evaluation, when the times_10_lazy
function
calls times_2(times_5(x))
, the times_2
function receives a promise with the unevaluated expression
times_5(x)
, and evaluates it only when it reaches line 9,
x * 2
(the expression gets evaluated in the correct
context, so there’s no naming collision of the x
variable).
It’s not only the call stack that has a surprising order with
times_10_lazy
– the temporal order the simulated work we’re
doing in the function (represented by the pause
blocks) is
different. The times_2
and times_5
functions
pause for 0.2 and 0.5 seconds, respectively. Those pauses occur in
opposite order in times_10
and
times_10_lazy
.
Keep in mind that lazy evaluation may result in counterintuitive
results in the flame graph. If you want to avoid some of the possible
confusion from lazy evaluation, you can use intermediate variables to
force the evaluation of arguments at specific locations in your code, as
we did in times_10
.
Why does profvis tell me the the wrong line is taking time?
In some cases, multi-line expressions will report that the first line
of the expression is the one that takes all the time. In the example
below, there are two for
loops: one with curly braces, and
one without. In the loop with curly braces, it reports that line 3,
containing the pause
is the one that takes all the time. In
the loop without curly braces, it reports that line 6, containing
for
, is the one that takes all the time, even though the
time is really spent on line 7, with the pause
.
For code that contains multi-line expressions like these, using curly braces will allow the profiler to identify the correct line where code is running.
How do I interpret memory profiling information?
The memory profiling information can be somewhat tricky to interpret, for two reasons. The first reason is that, compared to call stack information, memory usage information is collected with different temporal characteristics: call stack information is recorded instantaneously at each sample, while memory information is recorded between each sample.
The second reason that is that memory deallocations happen somewhat
randomly, and may happen long after the point where the memory was no
longer needed. The deallocations occur in garbage collection
(<GC>
) events.
For these reasons, it might look like a particular line of code (or function call in the flame graph) is responsible for memory allocation or deallocation, when in reality the memory use is due to a previous line of code.
If a section of code results in a large amount of allocation and deallocation, it means that it’s “churning” through memory and using a large amonut of temporary memory storage. This can be seen in Example 1 above. In these cases, it may be possible to optimize the code so that it doesn’t use as much temporary memory.
If a section of code results in a large amount of allocation but does not have a large amount of deallocation, then it means the memory is not being released. This could be because the code genuinely requires that extra memory, but it could also be a sign of a memory leak.
How do I split the panes vertically instead of horizontally?
The profvis examples in this document have a vertical split, but by default, profvis visualizations have a horizontal split. To switch directions, you can check or uncheck Options -> Split horizontally.
To change the split direction when the visualization opens, use
split="v"
:
How do I get source code to show with Rscript
?
If you run profvis from a script, the source code won’t show in the
source panel. This is because source refs are not recorded by default
when R is run non-interactively. To make it work, use
options(keep.source=TRUE)
. For example:
Rscript -e "options(keep.source=TRUE); p <- profvis::profvis({ profvis::pause(0.2) }); htmlwidgets::saveWidget(p, 'test.html')"
What are some other resources for profiling R code?
Base R comes with the Rprof
function (it’s what
profvis
calls to collect profiling data) as well as the
summaryRprof
function for getting a human-readable summary
of the profiling data collected by Rprof
.
Luke Tierney and Riad Jarjour have authored the proftools package, which provides many more tools for summarizing profiling data, including hot paths, call summaries, and call graphs. Proftools can also be used to generate a number of visualizations, including call graphs, flame graphs, and callee tree maps.