npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

motoko-benchmarking-ts

v1.0.0

Published

A set of functions to run benchmarking of any complexity for Motoko canisters in TypeScript, designed to be used in conjunction with module 'dfx-terminal-commands'.

Downloads

8

Readme

A set of functions to run benchmarks of any complexity for Motoko canisters in TypeScript and save the results to Excel tables. Designed to be used in conjunction with module 'dfx-terminal-commands'.

Can be used to measure performance of data structures, function calls, controlling the state of used resources of a canister, resource usage of different garbage collectors.

It allows to measure:

    rts_stable_memory_size: bigint; //Stable memory pages used
    rts_memory_size: bigint; //Total memory used by canister
    rts_total_allocation: bigint; //Total allocated memory of all time
    rts_reclaimed: bigint; //Total reclaimed memory of all time
    rts_heap_size: bigint; //Used Heap memory size of canister in bytes
    instruction_count: bigint; //Instruction cost of performing operations without costs for calling the function and returning values
    rts_collector_instructions: bigint; //Instructions that are used for garbage collection;
    rts_mutator_instructions: biging; //Total Wasm instructions used by the last called update function, except for garbage collection
    
    Note: if you want to measure GC instructions per function call, use flag "--force-gc" with the type if gc you want to use in dfx.json:
    "--generational-gc" - use generational GC,
    "--incremental-gc" - use incremental GC,
    "--compacting-gc" -  use compacting GC,
    "--copying-gc" - use copying GC (default)
    
    For example, if you want to measure the function resource usage with --incremental-gc:
    "defaults": {
    "build": {
      "args": "--force-gc --incremental-gc",
      ...
    }
  },

Results are a difference (delta) between the measured values before and after calling the update function, except for 'instruction_count' and 'rts_mutator_instructions': these values are measured per update function call.

'rts_collector_instructions' is also measured per function call, but since it's value depends on the state of the canister and gc is not usually run per each function call, we measure how much the called update function affects the cost of the next gc run.


Used libraries and references

Motoko library bench - main inspiration for this module. 'motoko-benchmarking-ts' can be considered my attempt to expand functionality and use cases of Motoko Benchmarking by porting it to TypeScript and giving it greater flexibility.

Improvements:

  • Number of measured parameters is expanded;
  • Support for complex testing scenarios due to increased flexibility;
  • Can be used with other TypeScript libraries and manipulate local dfx environment from code and not from terminal;
  • Can test standalone canisters which contain stable variables;
  • Can freely manipulate the results of tests to obtain more precise results (as shown in example later);
  • Can measure performances of a single function call or multiple ones (by adding or substracting measurement results);
  • Results can be saved to Excel which is useful for situations when we are bulk testing.

Motoko library Prim is used to get rts values.

Motoko library ExpimentalInternetComputer is used to measure instruction costs of operations inside the canisters.

@dfinity JavaScript libraries: agent, candid, identity-secp256k1 for interacting with local dfx environment.

Exceljs - for saving the results to Excel tables.

Usage


To be used properly, Motoko canisters should have this minimal interface:

actor {
    type RtsData = {
        rts_stable_memory_size: Nat;
        rts_memory_size: Nat;
        rts_total_allocation: Nat;
        rts_reclaimed: Nat;
        rts_heap_size: Nat;
        rts_collector_instructions: Nat;
        rts_mutator_instructions: Nat;
    };
    
    //Function for getting the values of Heap and Stable memory and Garbage Collector instructions
    public composite query func getRtsData() : async RtsData {
        return {
          rts_stable_memory_size = Prim.rts_stable_memory_size();
          rts_memory_size = Prim.rts_memory_size();
          rts_total_allocation = Prim.rts_total_allocation();
          rts_reclaimed = Prim.rts_reclaimed();           
          rts_heap_size = Prim.rts_heap_size();
          rts_collector_instructions = Prim.rts_collector_instructions();
          rts_mutator_instructions = Prim.rts_mutator_instructions();
        };
  };
}

Functions that you want to benchmark should look like this:

import IC "mo:base/ExperimentalInternetComputer";
...
public func example_function() : Nat {
        let count = IC.countInstructions(
            func () {
                //Some operations, whose cost you want to measure, for example:
                //var test = 0;
                //for (i in Iter.range(start_index, end_index)) {
                //  test := test + i;
                //}  
                }                
            }
        );
        return count;
    }

After you made the canisters that you want to test and added them to "dfx.json" use command dfx generate so that their declaration is generated for use in TypeScript.

Benchmarking with TypeScript files


For example, a canister that has functions for_loop() and add_batch():

...
import Map "mo:map/Map";

actor {
stable var t = Map.new<Nat, Nat>();

public func for_loop(start_index: Nat, total_elements: Nat) : async(Nat64) {
    let end_index = start_index + total_elements;
    let count = IC.countInstructions(
            func () {
              for (i in Iter.range(start_index, end_index)) {}    
            }
        );
    return count;
  };
  
  public func add_batch(start_index: Nat, total_elements: Nat) : async (Nat64) {
        let end_index = start_index + total_elements;
        let count = IC.countInstructions(
            func () {
                for (i in Iter.range(start_index, end_index)) {
                  Map.set(t, nhash, i, 1);
                }                
            }
        );
        return count;
  };
  
  public func delete_all() {
    Map.clear(t);            
  };
}

Then we can measure their resource usage:

Preparations for testing

import * from 'motoko-benchmarking-ts';
import * from 'dfx-terminal-commands';
//Importing interface and idlFactory from canister declaration
import { idlFactory, map } from "../../../src/declarations/map";

async function main() {
    //Starting dfx for testing
    await startDfx();
    //Setting a name that will be used later
    const canisterName = "map";
    //Generating random secp256k1 identity 
    const identity = makeIdentity();
    //Creating agent
    const agent = await makeAgent(identity);
    //Deploying canister and getting id
    await deployCanister(canisterName);
    const canisterId = await getCanisterId(canisterName);
    //Creating actor for calling canister 
    const actor: typeof memory_hashtable = makeActor(agent, idlFactory, canisterId);
    //Topping the canister up (fabricating ICP and transfering it's cycles value to canister)
    await fabricateIcpToCycles(canisterName, 1000000);
    ...

Example of Benchmarking

...
//======================================
//TESTING
//======================================
const testValues: bigint[] = [1n, 10n, 100n, 1000n, 10000n, 100000n, 1000000n, 10000000n];
//Making arrays for each function that we want to test, the more function you want to test, the more empty arrays of MeasurementData you should make
let testResults: MeasurementData[][] = [[], []];
 console.log(`+++++++++`);
console.log(`Beginning testing of canister: "${canisterName}"`);
//Measurements
for (let value of testValues) {
    console.log(`==============`);
    console.log(`Testing with ${value} elements`);
    console.log(`==============`);
    console.log(`Measuring for loop usage..`);
    //We pass in (actor, {function that we want to measure}, {arguments for function})
    let forLoopUsage = await measureFunction(actor, actor.for_loop, [0n, value]);
    console.log(`Measuring adding elements..`);
    let addData = await measureFunction(actor, actor.add_batch, [0n, value]);
    
    //Saving the for loop usage to subarray 0
    testResults[0].push(forLoopUsage);
    
    //When we call add_batch function, the resources used by for loop are also taken into account so if we want to know the exact cost of adding elements we can measure the cost of for loop without any other operations and then substract the obtained values from measurement results
    //Getting the usage of only Map.set(t, nhash, i, 1) operation, without for loop
    let purifiedAddData = purifyMeasurementData(addData, forLoopUsage);
    //Saving the purified data to subarray 1
    testResults[1].push(purifiedAddData);
    
    //Resetting the data structure
    await actor.delete_all();
}

//======================================
//WRITING TEST DATA TO TABLES
//======================================
console.log(`==============`);
console.log(`Saving the data to excel table: "${canisterName}.xlsx"..`);
console.log(`==============`);
//Generating headers for Excel table from test values
const headers = ['', ...testValues.map(value => value.toString())];
saveToExcel(`./results/${canisterName}.xlsx`, ["For loop", "Batch adding"], headers, testResults);

//Stopping the dfx service
await stopDfx();
console.log(`+++++++++++++`);
console.log(`All done!`);
}; //end of main()

main();

Custom usage


Functions for making custom benchmarks

//A function to get current rts data from an actor (if it complies with the needed interface)
async function getRtsData(actor: ActorSubclass<any>) : Promise<RtsData> 

//A function to measure the difference between the MeasurementData before and after update function call.
function measureDifference(new_values: RtsData, prev_values: RtsData, instruction_count: bigint) : MeasurementData

//A function to measure the difference between the RtsData before and after update function call.
export function measureDifferenceRts(new_values: RtsData, prev_values: RtsData) : RtsData

//A function for when you need to know the pure resource usage of operation by substracting resource usage of needed utils
function purifyMeasurementData(data: MeasurementData, base: MeasurementData) : MeasurementData 

//A function for getting a sum of all properties of 2 MeasurementData objects.
function addMeasurementData(data_1: MeasurementData, data_2: MeasurementData) : MeasurementData

//A function for getting a sum of all properties of 2 RtsData objects.
function addRtsData(data_1: RtsData, data_2: RtsData) : RtsData 

//A function to substract two MeasurementData objects.
function substractMeasurementData(data_1: MeasurementData, data_2: MeasurementData) : MeasurementData

//A function to substract two RtsData objects.
function substractRtsData(data: RtsData, base: RtsData) : RtsData 

//A function to save 2D array of RtsData to the Excel file.
async function saveToExcelRts(file_path: string, sheet_names: string[], headers: string[], data: RtsData[][])

//A function to save your custom data type to excel table. Useful if you want to conduct your own tests with different properties.
async function saveToExcelCustom<T extends Record<string, unknown>>(file_path: string, rows:string[], sheet_names: string[], headers: string[], data: T[][])

Examples of custom usage

Measuring multiple function calls resource usage:

...
let totalUsage: MeasurementData = {
    rts_stable_memory_size: 0n,
    rts_memory_size: 0n,
    rts_total_allocation: 0n,
    rts_reclaimed: 0n,
    rts_heap_size: 0n,
    instruction_count: 0n,
    rts_collector_instructions: 0n,
    rts_mutator_instructions: 0n
};
let numberOfCalls = 100;
for (let i = 0; i < numberOfCalls; i++) {
    let usage = await measureFunction(actor, actor.add_batch, [0n, 1n]);
    totalUsage = addMeasurementData(totalUsage, usage);
}

console.log(totalUsage);
...

Using RtsData:

let rtsDataPrev = await getRtsData(actor);
await actor.add_batch([0n, 1n]);
let rtsDataNew = await getRtsData(actor);

let rtsUsage = measureDifferenceRts(rtsDataNew, rtsDataPrev);
console.log(rtsUsage);