Profiling Cheatsheet
Table of content
This is a short overview - or reference - on how I tend to do quick application profiling.
Usually I use flamegraphs. First I gather a profile using language specific tooling or perf. That’s then converted into either a SVG or a format suitable for flamelens - a flamegraph viewer for the terminal.
Before diving in a short disclaimer that all profilers are liars. They give you data points to work with, but you have to keep in mind that they have biases and attribute time spent to the “wrong” functions. Don’t take the result at face value but do sanity checks and experiments when optimizing.
Java ¶
JFR ¶
The OpenJDK JVM includes the JFR profiling mechanism. You can
start profiling with the jcmd
tool:
jcmd <ProcessName|PID> JFR.start \
\
name=recording duration=1m \
settings=profile "filename=/tmp/profile.jfr"
# run workload to profile here
jcmd <ProcessName|PID> JFR.stop name=recording
You can convert the JFR into a flamegraph SVG using
jfr-converter.jar
from async-profiler
or you can view it directly using jmc.
async-profiler ¶
async-profiler is an alternative to JFRs that uses a different mechanism to collect data and it also includes non-java information. I often use it together with flamelens like this:
flamelens <(asprof -d 60 -o collapsed -e cpu <ProcessName>)
Or for quick ad-hoc profiling of test cases using nvim-jdtls and nvim-dap I have a keymap setup that roughly runs something like this:
-- Put this into a keymap or user-command
local asprof = "/path/to/async-profiler/lib/libasyncProfiler.so"
local vmArgs = string.format(
"-ea -agentpath:%s=start,event=cpu,collapsed,file=/tmp/profile.txt",
asprof
)
require("jdtls").test_nearest_method({
config_overrides = {
vmArgs = vmArgs,
noDebug = true,
},
after_test = function()
vim.cmd.tabnew()
vim.fn.jobstart({"flamelens", "/tmp/profile.txt", { term = true })
vim.cmd.startinsert()
end
})
To explain what’s going on in this function:
nvim-jdtls
provides the test_nearest_method
function which creates a
debug configuration for nvim-dap to run
the nearest function. The function takes two optional parameters.
config_overrides
allows to override the debug
configuration. The snippet here is using that to set vmArgs
to include the java agent from async-profiler
to trigger profiling on start.
The after_test
parameter allows to set a function which
gets executed when the debug session ends. In the snippet it creates a
new tab and runs flamelens
using the :terminal
feature.
Tests will often show a lot of activity in class initialization and JIT activity. Mockito is also a common sight. But if you drill down you can still get an idea of where the application is spending time.
Haskell ¶
Build the project with profiling enabled. For example with cabal:
cabal build --enable-profiling
Then run the application with the -p
RTS option:
cabal run --enable-profiling exes -- +RTS -p
This will create a file named after the executable with a
.prof
suffix in the current directory. You can process the
.prof
file using inferno and then view it
using flamelens:
flamelens <(inferno-collapse-guess <executable>.prof)
To instead get insights into heap allocations you can create an
eventlog file using the -l
RTS option and then process the
eventlog using eventlog2html.
You can also set extra -h
options to get more information.
Python ¶
cProfile ¶
Using cProfile:
python -m cProfile -o /tmp/myapp.profile myapp.py
To convert the output into the right format for flamelens use flameprof:
uvx flameprof --format log /tmp/myapp.profile | flamelens
For quick ad-hoc profiling using nvim-dap-python:
local dap = require("dap")
local function on_debugexit()
vim.cmd.tabnew()
local cmd = "uvx flameprof --format log /tmp/cProfile.prof | flamelens"
vim.fn.jobstart(cmd, { term = true })
vim.cmd.startinsert()
end
require("dap-python").test_method({
config = function(config)
local newconfig = vim.deepcopy(config)
newconfig.noDebug = true
newconfig.module = "cProfile"
newconfig.args = {
"-o", "/tmp/cProfile.prof",
"-m", config.module
}
vim.list_extend(newconfig.args, config.args)
dap.listeners.after.event_exited["cProfile"] = function(session)
if session.config.name == config.name then
vim.schedule(on_debugexit)
return true
end
end
return newconfig
end
})
To explain what’s going on here: Similar to nvim-jdtls, nvim-dap-python
provides a test_method
function to debug a nearby unit
test. Typically the function gets used without any parameters:
require("dap-python").test_method()
generates a debug
configuration based on the current context within neovim - file and
cursor location - and runs it using nvim-dap. But
test_method
also allows to extend or replace the generated
debug configuration before handing it over to nvim-dap. That’s
what the config
parameter is for.
The incoming config
will be for the test framework of
the project, either unittest
, pytest
or
django
and looks roughly like this:
config = {
name = "<TestCase>.<test_method>",
module = "unittest",
args = {
"tests.test_mymodule.TestCase.test_method"
},
request = "launch",
type = "python"
}
The logic in the above snippet exchanges the module
with
cProfile
and carries over the old module as arguments.
config = {
name = "<TestCase>.<test_method>",
module = "cProfile",
args = {
"-o", "/tmp/cProfile.prof",
"-m", "unittest",
"tests.test_mymodule.TestCase.test_method"
},
request = "launch",
type = "python"
}
It also adds a listener which gets triggered once the debug session
finishes. It opens the profile result in a new tab using flamelens. The listener
returns true
to have nvim-dap remove
the listener again, to ensure it only fires once.