canister-profiling

June 15, 2025 ยท View on GitHub

Profiling things in canisters.

Run

It's better to run dfx in background to see debug outputs and ic-repl calls in the same terminal and to clean everything happened before, allow the scripts to run.

dfx start --background --clean
chmod +x profile.sh profile_heap.sh profile_stable.sh

To run a separate benchmark:

./profile.sh vector

To profile heap call:

./profile_heap.sh vector
./profile_heap.sh array

or enumeration in comparison with rb_tree, etc.

To profile stable memory edit src/measure/stable.mo and call:

./profile_stable.sh

Note

--force-gc dfx option is required for heap and stable profiling.

Notes on benches

Time is measured in Wasm instructions per call. For most functions each call takes eaxactly the same amount of instructions. But in some cases there can be component to it that occurs sporadically. For example, add and removeLast for Buffer are vastly more expensive when the Buffer grows or shrinks its capacity. In those case the displayed value is the average over n calls, i.e. the sporadic overhead is amortized over n calls.

Memory is measured in bytes of heap size increase due to the call to the function or, in some cases, to n calls to the function.

In heap profiling:

  • heap size is the size without the garbage of the data structure returned by the profiled function.
  • gc size is the size of the garbage produced by the profiled function.
  • collector instructions is the number of instructions required to collect the garbage of the profiled function.
  • mutator instructions is the number of instructions for execution of the profiled function without garbage collector.

In stable profiling:

  • mutator instructions are the instructions for deserialization of data returned by the profiled function.
  • stable var query is the result of executing stableVarQuery function, the size of the serialized data.

Bench List against Refactored List

Instructions & heap

n = 100000

Time:

methodListRefactored
find196151
findIndex163191
all175146
any163151

Memory:

methodListRefactored
find17236
findIndex848
all2436
any836

Bench Vector against Buffer, Array

Instructions & heap

Testing for n = 100,000

Time:

methodvectorvector classbufferarray
init15151414
addMany1717--
clone188188298-
add336378561-
get20524713772
getOpt261303149-
put26630915282
size18322410169
removeLast315356397-
indexOf18218217356
firstIndexWith163163--
lastIndexOf222222180-
lastIndexWith203203--
forAll175175157-
forSome163163162-
forNone163163162-
iterate106106140-
iterateRev133133--
vals15615612720
valsRev163163--
items266266--
itemsRev292292--
keys105105--
iterateItems142142--
iterateItemsRev177177--
addFromIter406406357-
toArray155155118-
fromArray164164190-
toVarArray223223171114
fromVarArray16416419064
clear139180339-
contains18218216356
max17417419157
min17417419757
equal350350245133
compare391391286133
toText4544544010
foldLeft16316317669
foldRight190190193135
reverse426426244145
reversed412412244145
isEmpty11615712088
concat761-586-

Memory:

methodvectorvector classbufferarray
init408688409096400504400008
addMany408640408640--
clone425032425440553568-
add4160604160601659216-
get0000
getOpt000-
put0000
size0000
removeLast74047404553112-
indexOf282800
firstIndexWith88--
lastIndexOf20200-
lastIndexWith00--
forAll242448-
forSome8848-
forNone8848-
iterate8848-
iterateRev00--
vals204204480
valsRev6868--
items16001041600104--
itemsRev16000801600080--
keys4444--
iterateItems88--
iterateItemsRev00--
addFromIter4160604160601200008-
toArray400212400212400024-
fromArray408716409124600504-
toVarArray400212400212400008400008
fromVarArray408716409124600504400024
clear202040-
contains2828480
max3636480
min3636480
equal40840800
compare40840800
toText320019632001963199992296
foldLeft3636480
foldRight282800
reverse000400028
reversed4161444165520400028
isEmpty0000
concat812812-1800016-

Notes on Time:

  • Time is measured in Wasm instructions per call. For most functions each call takes eaxactly the same amount of instructions. But in some cases there can be component to it that occurs sporadically. For example, add and removeLast for Buffer are vastly more expensive when the Buffer grows or shrinks its capacity. In those case the displayed value is the average over n calls, i.e. the sporadic overhead is amortized over n calls.
  • Vector is a 2-dimensional array, hence we expect random access to be roughly twice as expensive as for Buffer/Array. More precisely, the outer array of a Vector is plain and the inner array is of an option type. Matching this fact, we can see in the get/put rows that the Vector cost is roughly the sum of the Buffer cost plus the Array cost.
  • Functions that iterate through a vector take advantage of the inner structure and eliminate the overhead a 2-step lookup. This can be seen in the rows indexOf, lastIndexOf, forAll, forSome, forNone, iterate, vals, addFromIter, toArray, fromArray, toVarArray, fromVarArray where Vector is performing close to Buffer.
  • The add row is an average over many additions. The reason that Vector performs better is that Buffer has an expensive O(n) allocation and copying operation each time the Buffer grows its capacity. Vector avoids copying of data blocks entirely. Vector only does allocation and copying in the order of O(sqrt(n)) for its index block.

Notes on Memory:

  • Memory is measured in bytes of heap size increase due to the call to the function or, in some cases, to n calls to the function.
  • The add row shows the garbage created by Buffer's growth events when the entire data is copied into a newly allocated array. Similarly removeLast produces garbage on shrink events.
  • The items function returns pairs. This leads to a heap allocations of 16 bytes per entry as we can see in the table.

Heap & GC profiling

methodheap sizegc sizecollector instructionsmutator instructions
vector40_097_98079_984377_979_7492_866_169_088
buffer47_835_24895_669_512460_218_0164_462_255_651
array40_000_12824375_004_331120_002_552

Serialization & Deserialization profiling

methodmutator instructionsstable var query
vector5_843_585_34520_082_525
array1_604_184_16210_000_038

Bench Enumeration against RBTree

Instructions & heap

Testing for n = 4096

methodenumerationred-black treeordered-mapzhusstable enumstable trie
random blobs outside average26742196224619122077573716
random blobs inside average21361605167410962140932323
root10609650000
leftmost251420390000
rightmost311625650000
min blob174612650000
max blob237318160000
min leaf236219630000
max leaf333026650000

min leaf in enumeration: 9

min leaf in red-black tree: 9

max leaf in enumeration: 16

max leaf in red-black tree: 16

Heap & GC profiling

methodheap sizegc sizecollector instructionsmutator instructions
enumeration278_848171_613_2484_349_0723_472_314_656
rb_tree377_172172_176_3127_690_5323_471_603_610

Serialization & Deserialization profiling

methodmutator instructionsstable var query
enumeration3_821_911_24337_732_293
rb_tree5_799_815_19238_780_862
stable_enumeration21_24257_936

Bench Sha2

Instructions & heap

The columns refer to the following code:

Columns 1,3,4 are comparable because they all perform Sha256. 1 block refers to 64 bytes of all 0xff. 0 blocks refers to the empty message.

Column 2 performs Sha512 and 1 block refers to 128 bytes of all 0xff.

Time:

methodSha256Sha512timohankeaviate-labs
0 blocks185043056249253798431
1 blocks235954221543423595601
10 blocks19120348908727453644
100 blocks18716341655317049325
1_000 blocks18671340894921848887

Memory:

methodSha256Sha512timohankeaviate-labs
0 blocks8001348264724376
1 blocks8642128234244104
10 blocks1624111882828010092
100 blocks103361020648083668152
1_000 blocks964721009588577836648488

Heap & GC profiling

methodheap sizegc sizecollector instructionsmutator instructions
sha25616013_025_7724_396124_716_639

Bench PRNG

Instructions & heap

Time:

methodSeiran128SFC64SFC32
next251377253

Memory:

methodSeiran128SFC64SFC32
next36488