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=recordingYou 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 more metrics:
asprof -d 60 -o jfr -e cpu,alloc,wall -f /tmp/profile.jfr <ProcessName>
jfrconv --alloc -o heatmap /tmp/profile.jfr /tmp/profile-alloc.html
jfrconv --cpu -o heatmap /tmp/profile.jfr /tmp/profile-cpu.html
jfrconv --wall -o heatmap /tmp/profile.jfr /tmp/profile-wall.htmlFor 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-profilingThen run the application with the -p RTS option:
cabal run --enable-profiling exes -- +RTS -pThis 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.pyTo convert the output into the right format for flamelens use flameprof:
uvx flameprof --format log /tmp/myapp.profile | flamelensFor 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.