Tutorial
Performance testing smart contracts developed within VS Code using Hyperledger Caliper
Analyze throughput, latency, and resource consumption of the blockchain network as the smart contract is being usedArchive date: 2024-01-28
This content is no longer being updated or maintained. The content is provided “as is.” Given the rapid evolution of technology, some content, steps, or illustrations may have changed.The IBM Blockchain Platform extension for VS Code helps developers create, test, and debug smart contracts. However, a key concern for developers remains: How performant is the developed smart contract?
Hyperledger Caliper is a blockchain benchmarking tool that's designed to perform benchmarks on deployed smart contracts, enabling analysis of throughput, latency, and resource consumption of the blockchain network as the smart contract is being used. This tutorial shows you how to use Hyperledger Caliper to performance test a Hyperledger Fabric smart contract that's developed and deployed using the IBM Blockchain Platform VS Code extension.
Prerequisites
To complete this tutorial, you will need:
- Microsoft VS Code with latest IBM Blockchain Platform extension installed
- To have completed the inbuilt IBM Blockchain Platform VS Code tutorial 1, which develops and deploys
demo-contract@0.0.1for interacting with assets of typeMyAsset
Estimated time
Once you've completed the VS Code tutorial, which should take about 20 - 30 minutes, you will build from the tutorial end point to:
- Obtain and configure Caliper
- Create Caliper test assets
- Run the performance benchmark
These subsequent steps should take approximately 30 - 40 minutes.
Steps
To complete this tutorial, you'll need to complete three main steps, detailed below:
- Obtain Caliper
- Create Caliper test assets
- Run the benchmark
Step 1. Obtain Caliper
In this tutorial, you will use the Caliper CLI, a node module that can be obtained and installed through npm. Once you've installed the Caliper CLI, you will need to bind the tool to a nominated SDK for a target blockchain. This tutorial has been produced using Caliper-CLI version 0.3.2 and Fabric-SDK-Node version 1.4.8.
Tasks:
Globally install the Caliper CLI module using the following terminal command:
npm install -g --only=prod @hyperledger/caliper-cli@0.3.2Bind to the desired SDK using the terminal command:
caliper bind --caliper-bind-sut fabric:1.4.8 --caliper-bind-args=-g
You can now use Caliper to benchmark a Fabric network using clients that are configured to use the nominated 1.4.8 SDK.
Step 2. Create Caliper test assets
Caliper requires two configuration files:
- A network configuration file that describes the system under test and provides connection requirements for the network
- A benchmark configuration that describes the performance benchmark workload and references user-specified test files
All required test assets will be created in a new directory, which acts as a workspace.
Tasks:
- Within VS Code, create a parent folder named caliper-workspace, containing two subfolders, named
networksandbenchmarksrespectively.
You will now populate these directories with the network configuration file and benchmark test the assets required by Caliper.
Network configuration file
The network configuration file describes the system under test and provides connection requirements for clients that interact with the network. It can be specified in either YAML or JSON format.
For Fabric networks, the network configuration file is an extended common connection profile, augmented by elements that are required by Caliper.
Tasks:
- Switch to the IBM Blockchain Platform extension window and disconnect from any gateways.
- Export the connection profile from VS Code:
- Under Fabric Gateways -> Org1 Local Fabric/Org1, right-click and select to export the connection profile.
- o Save this under the networks folder as
network_config.json.
- Export the local Fabric wallet from VS Code:
- Under Fabric Wallets -> Local Fabric Wallet -> Org1 Local Fabric/Org1, right-click and export the wallet.
- Save this under the networks folder as
myWallet.
Next, edit the exported connection profile to add the following required attributes to the top of the file:
{
"caliper": {},
"wallet": "",
"clients": {},
"channels": {}
}
Tasks:
- Open the exported connection profile
network_config.json. - Ensure the version is "1.0"
Identify the Distributed Ledger Technology (DLT) engine that is being tested. At the top of the file add a
caliperobject to the schema that contains a single property namedblockchainwith the string valuefabric."caliper": { "blockchain": "fabric" },Identify the wallet that contains the identities required to interact with the network:
- Add a key named
wallet. Within VS Code, right-click the wallet folder, select
Copy Path, and provide this as a value for thewalletproperty:"wallet": "<fully-qualified-path-wallet>",
- Add a key named
Specify the clients that Caliper can use to interact with the network by providing a mapping of client identities to client objects:
- Nest the existing
clientobject within a new JSON object that has the name of one of the identities in the exported wallet. In this scenario, the name isadmin. Nest the mapped identity created above within a new JSON Object named
clients."clients": { "admin": { "client": { "organization": "Org1MSP", "connection": { "timeout": { "peer": { "endorser": "300" }, "orderer": "300" } } } } },
- Nest the existing
Identify the channels that are available, and the smart contracts that are deployed to these channels:
- Add a
channelsobject to the schema under theclientsobject. - Within the
channelsobject, add another object namedmychannel, which is the name of the default channel created by VS Code and within which the smart contract is deployed. - Within the
mychannelobject, add a key namedcreatedwith the valuetrueto indicate that the channel already exists. - Within the
mychannelobject, add an array object namedchaincodes. Within this array, add an object containing key/value pairs for the ID and version of the deployed smart contract, which aredemo-contractand0.0.1respectively:
"channels": { "mychannel": { "created": true, "chaincodes": [ { "id": "demoContract", "version": "0.0.1" } ] } },- Add a
Save the modified file.
You now have a network configuration file that can be used by Caliper.
Benchmark configuration
A benchmark consists of repeatedly executing named test callback files over a series of rounds, with each round being controlled in duration and the load being driven during these rounds by (potentially multiple) clients that are themselves controlled through a rate control mechanism.
Benchmark configuration requires:
- One or more test callback files that interacts with the deployed smart contract and define the operation to be investigated
- A configuration file that defines the benchmark rounds and references the defined callbacks
Next, you will create a single test callback file for interacting with the deployed smart contract, and a configuration file that references the test callback file within a single test round.
The test callback file
The test callback file is the point of interaction with the deployed smart contract during the benchmark round. Each test callback file must export the following functions:
init-- used to initialise any required items for use in the run sectionrun-- used to interact with the smart contract method during the monitored phase of the benchmarkend-- used to clean up after completion of the run phase
The deployed smart contract contains the complete set of CRUD operations for an asset; for brevity, we will only investigate the ‘readMyAsset’ smart contract method.
The Caliper blockchain object uses the following methods to interact with a deployed smart contract:
invokeSmartContract(ctx,contractId,contractVersion,args)querySmartContract(ctx,contractId,contractVersion,args)Where:
ctxis a user contextcontractIdis the smart contract namecontractVersionis the smart contract versionargsis an object that contains:chaincodeFunction-- the name of the smart contract function to callinvokerIdentity-- the identity to use when performing the function callchaincodeArguments-- an array of arguments to pass to the function when it is being called
Here is a template for a basic test callback that interacts with the deployed smart contract named demo-contract at version 0.0.1:
'use strict';
module.exports.info = 'Template callback';
const contractID = 'demo-contract';
const version = '0.0.1';
let bc, ctx, clientArgs, clientIdx;
module.exports.init = async function(blockchain, context, args) {
};
module.exports.run = function() {
};
module.exports.end = async function() {
};
Tasks:
- Create a subfolder named
callbackswithin thebenchmarksfolder. - Create a file called
queryAssetBenchmark.jsin the callbacks folder. - Open the file and insert the above template code.
- Save the file.
Now it's time to populate the init, run, and end template functions.
init
This function is used to persist passed arguments and prepare any items that are required within the run function. At a minimum, you need to persist the blockchain and context arguments, but since the readMyAsset function requires a set of assets to query, you also need to create those. You can pass a set of user-specified arguments to the init function, which means you can specify the number of assets to create during the test as a variable. It should be noted that since multiple clients can be used during the benchmark round, and they will all call the same test callback, it's important to disambiguate between each client. This is most easily achieved by using the unique client identifier that is a property of the context.
Tasks:
- Persist blockchain, context, and args as global variables
bc,ctx, andclientArgs, respectively. - Assume that the number of desired assets to create is given as
clientArgs.assets, and create aforloop that's bounded between 0 and the number of assets to be created. - You will create assets within the
forloop using the smart contract methodcreateMyAsset. Since the method may throw if an error occurs, you should condition for that within a try-catch block and print the error to the console to ease debugging.- Create a try-catch block in the
forloop. - In the catch, add an information statement reporting the error.
- In the try, await the completion of an
invokeSmartContractcall on the blockchain object, passing the known context, contract name, contract version, and an object that contains:chaincodeFunctionset ascreateMyAssetinvokerIdentityset asadmin, an identity in the exported walletchaincodeArgumentswith an array that contains:- a unique asset identity that's formed by the client identifier and the current
forloop index - a string to persist under the asset identity
- a unique asset identity that's formed by the client identifier and the current
- Create a try-catch block in the
module.exports.init = async function(blockchain, context, args) {
bc = blockchain;
ctx = context;
clientArgs = args;
clientIdx = context.clientIdx.toString();
for (let i=0; i<clientArgs.assets; i++) {
try {
const assetID = `${clientIdx}_${i}`;
console.log(`Client ${clientIdx}: Creating asset ${assetID}`);
const myArgs = {
chaincodeFunction: 'createMyAsset',
invokerIdentity: 'admin',
chaincodeArguments: [assetID, `UUID: ${assetID}`]
};
await bc.bcObj.invokeSmartContract(ctx, contractID, version, myArgs);
} catch (error) {
console.log(`Client ${clientIdx}: Smart Contract threw with error: ${error}` );
}
}
};
run
This is the function that is run repeatedly in the recorded benchmark test phase, consequently it should be as concise as possible. Your goal is to evaluate the readMyAsset smart contract function, performing a query on one of the assets that were created within the init phase. The function must return an unresolved promise and not block -- this enables the driving client to make multiple concurrent run calls.
Tasks:
- Create a string identity for the asset to query, formed by the concatenation of the test client index and a random integer between 0 and the number of created assets.
- Return the call on
querySmartContract, passing the known context, contract name, contract version, and an object that contains:chaincodeFunctionset asreadMyAssetinvokerIdentityset asadmin, an identity in the exported walletchaincodeArgumentswith an array that contains the asset to query in this invocation
module.exports.run = function() {
const randomId = Math.floor(Math.random()*clientArgs.assets);
const myArgs = {
chaincodeFunction: 'readMyAsset',
invokerIdentity: 'admin',
chaincodeArguments: [`${clientIdx}_${randomId}`]
};
return bc.bcObj.querySmartContract(ctx, contractID, version, myArgs);
};
end
The end function is used to clean up after a test. To ensure test repeatability, you need to delete all assets created within the init phase. You can use the same for loop from the init phase, modified to call the smart contract function deleteMyAsset and passing only the asset identity to delete.
module.exports.end = async function() {
for (let i=0; i<clientArgs.assets; i++) {
try {
const assetID = `${clientIdx}_${i}`;
console.log(`Client ${clientIdx}: Deleting asset ${assetID}`);
const myArgs = {
chaincodeFunction: 'deleteMyAsset',
invokerIdentity: 'admin',
chaincodeArguments: [assetID]
};
await bc.bcObj.invokeSmartContract(ctx, contractID, version, myArgs);
} catch (error) {
console.log(`Client ${clientIdx}: Smart Contract threw with error: ${error}` );
}
}
};
You have now completed the specification of a test callback, which creates test assets in the init phase, queries the created assets in the run phase, and deletes the test assets in the end phase.
The benchmark configuration file
The benchmark configuration file defines the complete performance test to be run against the deployed smart contract. It can be specified in either YAML or JSON format and specifies:
- The number of test clients to use when generating the test load
- The number of test rounds
- The duration of each round
- The load generation method during each round
- The callback (test interaction) to use within each round
Now you'll start building a YAML benchmark configuration file that uses the queryAsssetBenchmark.js test callback. Note that YAML files are case sensitive; all labels are to be specified in lower case.
Tasks:
- Create a new file named
myAssetBenchmark.yamlwithin thebenchmarksfolder and open the file for editing. - Add a root level literal block named
testthat describes the test to run and contains:- A
namekey with the valuemy-asset-benchmark - A
descriptionkey with a short description as the value - A literal block named
workersthat defines the type and number of test workers to use. For now, add the following key/value pairs:- type: local
- number: 2
- A literal block named
roundsthat is left blank
- A
- Add a root-level literal block named
monitorthat contains a single key namedtypewith a single array entry ofnoneas a value. This indicates that you will not be performing any resource monitoring during the benchmark testing. Add a root-level literal block named
observerthat contains two keys namedtypeandintervalwith valueslocaland 5 respectively. This indicates that you will be observing the test progression every 5 seconds using local statistics.--- test: name: my-asset-benchmark description: Benchmarking for VS Code sample workers: type: local number: 2 rounds: monitor: type: - none observer: type: local interval: 5
The rounds literal block contains each benchmark test round that is to be run, in a sequence format, headed by a unique round label. Rounds may be used to benchmark different smart contract methods, or the same method in a different manner. Each test round block contains the following:
label-- a unique label to use for the rounddescription-- a description of the round being runchaincodeId-- the chaincode (smart contract) ID under test[txDuration | txNumber]-- a specifier for the length of the round, which may be duration or transaction basedrateControl-- a rate control method with optionscallback-- a workspace relative path to a user-defined test file for the smart contract that is being investigatedarguments-- an optional array of arguments to be passed to the user test file (callback) when being invoked
You will now populate these.
Tasks:
- Start a new sequence with a key named
labeland the value ‘queryAsset’. - Within the
queryAssetsequence, add a key nameddescriptionwith the value "Query asset benchmark test." - Within the
queryAssetsequence, add a key namedchaincodeIdwith the valuedemo-contract. - Within the
queryAssetsequence, add a literal block namedtxDurationwith a single sequence entry of 30. This indicates that the benchmark test will be run once for 30 seconds. - Within the
queryAssetsequence, add a literal block namedrateControlthat contains a single sequence entry with:- A key named
typewith the string value offixed-backlog. This indicates that you will be driving the benchmark to maintain a fixed transaction backlog of pending transactions. - A literal block named
opswith a key namedunfinished_per_clientthat has the value 2. This indicates that each client will be driven at a rate so as to maintain 2 pending transactions.
- A key named
- Within the
queryAssetsequence, add a callback with a relative path to thequeryAssetBenchmark.jsfile. The relative path is between the benchmark file being created and the callback file. Within the
queryAssetsequence, add a literal block namedarguments. Add a single key namedassetswith the value 10. This will be passed to the test callback during theinitphase.--- test: name: my-asset-benchmark description: Benchmarking for VS Code sample workers: type: local number: 2 rounds: - label: queryAsset description: Query asset benchmark test chaincodeId: demo-contract txDuration: 30 rateControl: type: fixed-backlog opts: unfinished_per_client: 2 callback: benchmarks/callbacks/queryAssetBenchmark.js arguments: assets: 10 monitor: type: - none observer: type: local interval: 5
You now have a benchmark configuration file with companion test callback files that can be used by Caliper.
Step 3. Run the benchmark
You will now use the Caliper CLI to complete a performance benchmark against the default IBM Blockchain Platform VS Code network, using the resources that you created in the preceding steps. The command to be issued is caliper benchmark run, and it must be provided with details of the network configuration file, the benchmark configuration file, and the workspace that is being used. Based on the resources that you have created, you must supply the following argument pairings:
- caliper-networkconfig: networks/
network_config.json - caliper-benchconfig: benchmarks/
myAssetBenchmark.yaml - caliper-workspace:
./
Since the network has already been configured with chaincode installed and instantiated, the only action that Caliper needs to perform is the test phase, using a fabric gateway that has discovery enabled. To specify these options, you will pass the following additional flags to the CLI command:
caliper-flow-only-testcaliper-fabric-gateway-usegatewaycaliper-fabric-gateway-discovery
Tasks:
Ensure that you are in the
caliper-workspace directorydirectory, within which the following resources should now exist:. ├── benchmarks │ ├── callbacks │ │ └── queryAssetBenchmark.js │ └── myAssetBenchmark.yaml └── networks ├── myWallet └── network_config.jsonRun the Caliper CLI command
caliper launch master --caliper-benchconfig benchmarks/myAssetBenchmark.yaml --caliper-networkconfig networks/network_config.json --caliper-workspace ./ --caliper-flow-only-test --caliper-fabric-gateway-usegateway --caliper-fabric-gateway-discovery
You will see the operation of Caliper on the console as the testing progresses, culminating in a summary output of the benchmark. An HTML report will also be generated containing the same information that was printed to the console during the benchmarking process.
The report will detail the following items of information for each benchmark round:
- Name -- the round name, which correlates to the test round label from the benchmark configuration file
- Succ/Fail -- the number of successful/failing transactions
- Send Rate -- the rate at which Caliper issued the transactions
- Latency (max/min/avg) -- statistics relating to the time taken in seconds between issuing a transaction and receiving a response
- Throughput -- the average number of transactions processed per second
Summary
You have now successfully benchmarked the deployed smart contract on the default local network available from the IBM Blockchain Platform VS Code extension. You can repeat the test varying the benchmark parameters: For information on the full set of parameters, please see the official Caliper documentation.