3. How to enable collection of performance statistics (profiling)
When a Haskell application is slow or uses too much memory, Cabal and GHC can help you understand why. The main steps are:
Configure the project in a way that makes GHC insert performance-measuring code into your application.
Run the application with the right runtime system (RTS) flags to produce a performance report.
Visualize and analyze that report.
The process of inserting performance measuring code and collecting performance information is called “profiling”. This guide describes how to instruct Cabal to pass desired profiling flags to the GHC compiler; Cabal acts as a convenient build configuration interface while the work is done by GHC. To get a deeper understanding of the overall profiling process itself in GHC, it is highly recommended to read in depth the Profiling section in GHC’s User Guide.
3.1. Profiling CPU performance
First, configure Cabal to build your application, e.g. my-app
, with profiling enabled,
with the following command:
$ cabal configure --enable-profiling
This command creates a cabal.project.local
file with the following content:
profiling: True
This file stores temporary configuration settings that are passed implicitly to further Cabal commands
like cabal build
and cabal run
.
The setting profiling: True
tells GHC to build your application (and its dependencies) with profiling enabled,
and to insert performance measuring code into your application.
Where exactly such code is inserted can be controlled with settings like profiling-detail
that are presented later.
Further in-depth information on profiling with GHC and its compiler options can be found in the
GHC profiling guide
Note
While a cabal.project file is intended for long-time settings
that are useful to store in Git, cabal.project.local
is for short-lived, local experiments
(like profiling) that, in general, shouldn’t be committed to Git.
Second, run your application with the right runtime system flags and let it create a profiling report:
$ cabal run my-app +RTS -pj -RTS
<app builds, runs and finishes>
When the application finishes, a profiling JSON report (due to option -pj
)
is written to a <app-name>.prof
file, i.e. my-app.prof
, in the current directory.
Note
Different report formats can be generated by using different RTS flags. Some useful ones are:
-p
for a GHC’s own standard report<app-name>.prof
, which can be visualized with profiteur or ghcprofview.-pj
for a JSON report<app-name>.prof
, which can be visualized with Speedscope.-l -p
for a binary “eventlog” report<app-name>.eventlog
, which contains a lot more details and can show you resource usage over time, and can be converted to JSON with hs-speedscope to be visualized with Speedscope. This will also generate a.prof
file (due to-p
), which you can ignore. We just need the-p
flag for the.eventlog
file to include profiling information.
Finally, visualize this JSON report my-app.prof
and analyze it for performance bottlenecks.
One popular open-source
flame graph
visualizer is
Speedscope,
which runs in the browser and can open this JSON file directly.
See the
Haskell Optimization Handbook
on how to optimize your code based on the profiling results afterwards.
So far, we’ve only used a single Cabal option to enable profiling in general for your application.
Where and when GHC should insert performance measuring code can be controlled with the profiling-detail
setting
and ghc-options
.
Leaving profiling-detail
unspecified as before results in sensible defaults that differ between libraries and executable.
See the docs for profiling-detail to see which options are available.
You can provide profiling-detail
settings and more compiler flags to GHC
(such as -fno-prof-count-entries
) via the cabal.project.local
file:
profiling: True
profiling-detail: late-toplevel
program-options
ghc-options:
<further options>
The setting profiling-detail: late-toplevel
instructs GHC to use so-called
late-cost-center profiling
and insert measuring code only after important optimisations have been applied to your application code.
This reduces the performance slow-down of profiling itself and gives you more realistic measurements.
The program-options
section allows you to add more settings like GHC options to the local
packages of your project (See Program options).
The ghc-options
setting allows you to further control which functions and other bindings
the GHC compiler should profile, as well as other aspects of profiling.
You can find more information and further options in the
GHC “cost-center” guide.
and the
GHC profiling compiler options
section.
3.2. Profiling your dependencies too
The profiling setup so far with the cabal.project.local
file only applied to your local packages,
which is usually what you want.
However, bottlenecks may also exist in your dependencies, so you may want to profile those too.
First, to enable late
-cost-center profiling for all packages (including dependencies) concerning your project,
not just the local ones, add the following to your project’s cabal.project.local
file:
package *
profiling-detail: late-toplevel
Note
There are several keywords to specify to which parts of your project some settings should be applied:
program-options
to apply to all local packages.package <package-name>
to apply to a single package, be it local or remote.package *
to apply to all local and remote packages (dependencies).
Second, rerun your application with cabal run
, which also automatically rebuilds your application:
$ cabal run my-app -- +RTS -pj -RTS
Resolving dependencies...
Build profile: -w ghc-9.10.1 -O1
In order, the following will be built (use -v for more details):
- base64-bytestring-1.2.1.0 (lib) --enable-profiling (requires build)
- cryptohash-sha256-0.11.102.1 (lib) --enable-profiling (requires build)
...
<app runs and finishes>
You can now find profiling data of dependencies in the report my-app.prof
to analyze. More information on how to configure Cabal options can be found in the
Cabal options sections.