Skip to Content
Scalus Club is now open! Join us to get an early access to new features 🎉

Profiling Cardano Smart Contracts

Scalus includes a built-in CEK machine profiler that shows where execution budget is spent — by source location and by builtin function.

Quick Start

Replace evaluateDebug with evaluateProfile to collect profiling data:

import scalus.uplc.eval.{ProfileFormatter, PlutusVM} given PlutusVM = PlutusVM.makePlutusV3VM() val result = compiled.toUplc().evaluateProfile // result.isSuccess, result.budget, etc. work as usual assert(result.isSuccess) // Access profiling data result.profile.foreach { profile => println(ProfileFormatter.toText(profile)) }

Output

ProfileFormatter.toText produces two tables:

=== Profile by Source Location === location count mem cpu MyValidator.scala:42 153 153000 24480000 Prelude/List.scala:903 87 87000 13920000 === Profile by Function === function count mem cpu HeadList 74755 2392160 6215878250 UnIData 45025 1440800 933998600 MkCons 42126 1348032 3048321612 Total: mem=185927967 cpu=49131853260

By Source Location shows which lines of your Scala source code are most expensive. By Function shows which UPLC builtin functions consume the most budget.

Both tables are sorted by memory descending.

HTML Output

For a richer view with heat-bar visualization:

import java.nio.file.{Files, Paths} result.profile.foreach { profile => Files.writeString(Paths.get("profile.html"), ProfileFormatter.toHtml(profile)) }

Machine-Readable Output (profile.json + manifest)

PlutusScriptEvaluator can render profiles to files on every evaluation — that covers everything that runs through the ledger: Emulator, TxBuilder, transaction validation. (For tests that evaluate a compiled program directly, see Profiling Direct UPLC Runs below.) The easiest switch is the environment variable (see Project Commands):

SCALUS_PROFILE=full sbt test

For each profiled script run this writes, into the report output directory (SCALUS_DUMP_DIR, default: target/scalus):

  • <scriptHash>-<tag>-<index>.profile.html – the interactive report
  • <scriptHash>-<tag>-<index>.profile.json – machine-readable data ("schemaVersion": 1)
  • <scriptHash>-<tag>-<index>.profile.csv – flat table for spreadsheets
  • profile-manifest.json – the discovery entry point listing every run

File names are stable per (scriptHash, redeemer tag, index), so repeated evaluations (e.g. during fee balancing) overwrite rather than accumulate. Manifest entries are merged on that same key: a re-evaluated script updates its entry in place, while every other run already in the directory is preserved. The manifest maps each run to its files:

{ "schemaVersion": 1, "runs": [ { "scriptHash": "ab12…", "language": "PlutusV3", "redeemer": { "tag": "Spend", "index": 0 }, "budget": { "mem": 185927967, "cpu": 49131853260 }, "files": [ { "format": "html", "file": "ab12…-Spend-0.profile.html" }, { "format": "json", "file": "ab12…-Spend-0.profile.json" }, { "format": "csv", "file": "ab12…-Spend-0.profile.csv" } ] } ] }

Tools should start from profile-manifest.json and check schemaVersion (currently 1 for both the manifest and profile.json). The Scalus VS Code extension consumes profile.json to annotate source lines with per-line cost.

The report directory lives under target/, which is git-ignored. Editor tooling that discovers files through the workspace search index will not see it — VS Code’s findFiles honours ignore files. The Scalus Profiler extension handles this from 0.2.2 onwards; older versions find nothing unless you point SCALUS_DUMP_DIR at a directory that is not ignored.

Profiling Direct UPLC Runs

Suites that evaluate a compiled program themselves — runScript, evaluateDebug, runWithDebug — never reach PlutusScriptEvaluator, so SCALUS_PROFILE=full writes no files for them. ScalusTest provides runWithProfileReport for that case: it evaluates with profiling on and emits the same renderings, into the same directory, indexed in the same manifest.

class VestingValidatorTest extends AnyFunSuite, ScalusTest { test("Successful multiple partial withdrawals") { val result = compiled.scriptV3().runWithProfileReport(scriptContext) assert(result.isSuccess) } }

The redeemer tag in the file name follows the script purpose in the ScriptContext, so a spending validator lands in <scriptHash>-Spend-0.profile.* just as it would from the ledger.

Calling the method is itself the request for a report, so it writes the full set by default. SCALUS_PROFILE, SCALUS_PROFILE_OUT and SCALUS_DUMP_DIR override that exactly as they do for the ledger — SCALUS_PROFILE=off silences the output without touching the test. Because manifest entries merge, ledger- and test-produced runs accumulate side by side and the extension lists them together.

Toggle Profiling in Tests

Add a flag to switch between evaluateDebug and evaluateProfile without changing test assertions:

class MyValidatorTest extends AnyFunSuite, ScalusTest { val profilingEnabled = false // flip to true when needed extension (term: scalus.uplc.Term) private def eval(using PlutusVM): Result = if profilingEnabled then val result = term.evaluateProfile result.profile.foreach(p => info(ProfileFormatter.toText(p))) result else term.evaluateDebug test("validator budget") { val result = compiled.toUplc().eval // uses evaluateDebug or evaluateProfile assert(result.isSuccess) } }

The result.profile.foreach line is safe to leave permanently — when profiling is off, profile is None.

This prints to the test log. When you want files an editor can read instead, use runWithProfileReport.

PlutusVM Methods

For script-level profiling (with CIP-117 validation):

val result = vm.evaluateScriptProfile(program.deBruijnedProgram) // result.profile contains profiling data

Performance

Profiling adds ~40% overhead to CEK evaluation (measured on the Knights benchmark: 6s vs 8.4s). When profiling = false (the default), there is zero overhead.

See Also

  • JS/TS Profiling — Collect profiling data (JSON) from JavaScript/TypeScript with Scalus.evaluateScriptProfile
  • Debugging — IDE debugging, logging, error traces
  • Unit Testing — Test validators with budget assertions
Last updated on