Profiling Hydra scripts
This tutorial explains how to profile Hydra scripts and is intended for contributors to the hydra-node
.
Overview
For every pull request and the latest master
branch, we compute typical transaction costs in terms of size, memory, and CPU usage of the Hydra protocol transactions on Cardano. You can view the latest results here.
Such benchmarks provide a comprehensive overview of the constraints for a given transaction, including maximum transaction size and percent of maximum memory and CPU budget. For a detailed assessment, we analyze all scripts that run within a given transaction.
To gain detailed insights into what exactly results in excessive memory or CPU usage, we need to profile the scripts as they validate a transaction.
Follow the instructions provided by the Plutus
project here, adapted for the hydra
codebase.
Isolating a transaction to profile
First, isolate the specific Cardano transaction you want to profile. For example, let's investigate what the collectCom
transaction
for 5
parties in the tx-cost
benchmark is spending most time and memory on.
The benchmark computes many transactions with the growing number of participants in computeCollectComCost
:
computeCollectComCost =
catMaybes <$> mapM compute [1 .. 100]
where
compute numParties = do
(st, tx) <- generate $ genCollectComTx numParties
let utxo = getKnownUTxO st
case checkSizeAndEvaluate tx utxo of
-- [...]
Here, isolate the transaction for 5
parties by altering the function to maybe [] pure <$> compute 5
.
Compiling a script for profiling
The collectCom
transaction utilizes the vCommit
and vHead
validator scripts. To enable profiling, add the following directive to the modules Hydra.Contract.Commit
and Hydra.Contract.Head
:
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:profile-all #-}
Acquiring an executable script
You can achieve this using
prepareTxScripts
.
To acquire and save the fully applied scripts from the transaction onto disk, run:
-- [...]
(st, tx) <- generate $ genCollectComTx numParties
let utxo = getKnownUTxO st
scripts <- either die pure $ prepareTxScripts tx utxo
forM_ (zip [1 ..] scripts) $ \(i, s) -> writeFileBS ("scripts-" <> show i <> ".flat") s
-- [...]
After running the corresponding code (tx-cost
in our example), you will have
scripts-{1,2,3,4,5}.flat
files in the current directory.
Unfortunately, it's quite hard to distinguish them, but script sizes should help in identifying the larger vHead
script from the smaller vCommit
script. In the profile, the names of original plutus-tx
functions will be retained, which should make it clear at the latest.
Running the script and analyzing the results
To perform this step, use the following tools available through Nix:
nix shell nixpkgs#flamegraph github:input-output-hk/plutus#x86_64-linux.plutus.library.plutus-project-924.hsPkgs.plutus-core.components.exes.traceToStacks github:input-output-hk/plutus#x86_64-linux.plutus.library.plutus-project-924.hsPkgs.plutus-core.components.exes.uplc
To produce the profile log as explained above, you need to use a different input format since prepareTxScripts
retains the original name annotations.
uplc evaluate -t -i scripts-1.flat --if flat-namedDeBruijn --trace-mode LogsWithBudgets -o logs
Check for a logs
file output. If not present, ensure the script was compiled with profiling enabled as specified.
Finally, you can inspect the logs or generate flame graph SVGs as outlined in the original tutorial:
cat logs | traceToStacks | flamegraph.pl > cpu.svg
cat logs | traceToStacks --column 2 | flamegraph.pl > mem.svg
Here's an example of a memory profile for a 5
party collectCom
:
Open the SVG in a browser to interactively search and explore the profile in detail.