Hyperledger Fabric is the permissioned blockchain framework which belongs to the Hyperledger family. It gives the possibility to implement chaincode which is basically the smart contract (just named in Fabric terminology). Currently Fabric allows to implement chaincode in 3 different languages:
- Go
- Javascript (Node.js)
- Java (based on Netty)
Choosing one of the given technologies can impact development process and among the others: the performance of the solution. I would like to compare the performance of the chaincode implemented in the Node.js and Java.
Prerequisites
Tests are done on Dell laptop, i7-8650, 1.9Ghz, 16gb RAM. My example Fabric blockchain network consist of 6 peer nodes (3 orgs, each has 2 peers), so there are 6 application docker images running (1 application image per 1 peer node). CouchDB is used as state database. Single orderer based on Kafka.
Test scenario
The test scenario is to run 300 calls to the chaincode which should result in modifying single JSON object in the CouchDB. The id of the document is not changed – this is why first call to the chaincode results in saving the document, all the next calls should overwrite the content of the document in the CouchDB (and of course be legit blockchain transaction). This is the example content of JSON which will be saved:
"revisions": [ { "parties": [ { "id": "\n\u0007Org1MSP\u0012u0007-----BEGIN CERTIFICATE-----\nMIICcTCCAhegAwIBAgIQZaTUB3DdVh84jbL+xuSDtjAKBggqhkjOPQQDAjCBoTEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xMDAuBgNVBAoTJ29yZzEuc2VydmljZS5kZXYtZXUtd2VzdC0xLmF3\ncy5sdC50cmFuczEzMDEGA1UEAxMqY2Eub3JnMS5zZXJ2aWNlLmRldi1ldS13ZXN0\nLTEuYXdzLmx0LnRyYW5zMB4XDTE5MDYxMjA5MzgwMFoXDTI5MDYwOTA5MzgwMFow\ngYMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ8wDQYDVQQLEwZjbGllbnQxNjA0BgNVBAMMLUFkbWluQG9y\nZzEuc2VydmljZS5kZXYtZXUtd2VzdC0xLmF3cy5sdC50cmFuczBZMBMGByqGSM49\nAgEGCCqGSM49AwEHA0IABJhzdKcmKXd4mkDH2Ps7H4qQib0rz4panrG5nV54KHho\nZGVyyxYzLD4VR6qFzmMtliYL15b3KOXtfg0Z9MKx8kajTTBLMA4GA1UdDwEB/wQE\nAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIIyPDjpL+OgZuRcK/miefKP6\nZDmhCP2Bouv6vV/Z2Y5oMAoGCCqGSM49BAMCA0gAMEUCIQCqNVY36IM8zX0Xfi51\nh8MtDI7YiPd5p7VVlTfEkMHI4wIgPsph7c0H214azeOefGu/g/U+TEzHgz3aWD+e\nZ69oApc=\n-----END CERTIFICATE-----\n", "role": "CREATOR" }, { "id": "LAJDSFLKAJSDFLKJSA_EXAMPLE_KEY", "role": "CREATOR" } ], "payloadUrl": "http://payload/url/cccc", "timestamp": 1561108248620 } ],
So the given content will be saved in the single CouchDB document 300 times (saved in the CouchDB and appended to the Fabric blockchain).
Java
The client using fabric-node-sdk calls our chaincode invoking thecreateRevision
function. The content of the function implemented as the Java chaincode is given:
public Response createRevision(ChaincodeStub stub, List<String> args) { byte[] existingRevision = stub.getState(args.get(0)); try { byte[] bytes = objectMapper.writeValueAsBytes(new RevisionsCatalog(args.get(0), Arrays.asList( new Revision(args.get(1), new Date(), Arrays.asList( new RevisionParty(new String(stub.getCreator(), Charset.forName("UTF-8")), RevisionPartyRole.CREATOR), new RevisionParty(args.get(2), RevisionPartyRole.CREATOR) )) ))); stub.putState("1", bytes); System.out.println("Added! " + sdf.format(new Date())); return newSuccessResponse("hey", bytes); } catch (JsonProcessingException e) { e.printStackTrace(); } return newErrorResponse("failed"); }
The function overview:
- Reads the state, getting the
existingRevision
object with given ID passed as argument. This step is generally just for checking the impact of reading the state on the performance of the chaincode. It has not influence on the rest of the logic of the chaincodecreateRevision
function. RevisionsCatalog
is master model class and some other classes are used to model the domain resulting the JSON content we want to save to the chaincode. TheobjectMapper
is the Jackson mapper used to get Java model objects as JSON bytes array.- State is saved (once saved, then overwrite).
- The content of JSON saved as the state is then returned.
Node.js
The Node.js typescript based chaincode:
async createRevision(stub: ChaincodeStub, args: string[]): Promise<Buffer> { const existingRevision: Buffer = await stub.getState(args[0]); const revisionCatalog: RevisionCatalog = RevisionCatalogFactoryService.newRevisionCatalog( args[0], args[1], stub.getCreator().getIdBytes().toString('utf8'), args[2]); const revisionCatalogString: string = JSON.stringify(revisionCatalog); const revisionCatalogBuffer = Buffer.from(revisionCatalogString); await stub.putState(args[0], revisionCatalogBuffer); logInfo(`Added! ${new Date()}); return revisionCatalogBuffer; }
The function behavior is the copy of Java chaincode behavior. First it reads the state, then it creates the revisionCatalog
which later saved under the id, passed as function arg (but as said id is always the same in our tests).
Test results
The test logic is given:
for (let i=0;i<300;i++) { transactionCall(); // implemented chaincode transaction }
The result was done 6 times for every chaincode. The performance – time needed for processing 300 transactions is measure of difference of time between the first logged message in the chaincode and the last logged message in the chaincode.
Java:
Test 1: 930ms
Test 2: about 50% of chaincode requests were not processed
Test 3: 2476ms
Test 4: 998ms
Test 5: 2164ms
Test 6: 1822ms
Node.js
Test 1: 2722ms
Test 2: 2037ms
Test 3: about 50% of chaincode requests were not processed
Test 4: 1212ms
Test 5: 1573ms
Test 6: 1223ms
Results interpretation
The tests show that Java is a little bit faster, however taking into consideration all tests I did I would not say it’s the rule. I would say that both implementation are pretty the same fast.
The wired thing about the results is that in both cases there can be huge differences between the tests. Once we have about 1 second, then more than 2 seconds. Generally the request processing (so the function processing) is similar in both Java and Node.js implementations. In both cases there happened situations where time between invocation (visible as log) could be even hundreds of milliseconds. In such cases the total time of execution was relatively long (even almost 3 seconds). Example logs logged by Node.js chaincode:
2019-06-21T11:43:25.996Z info [lib/handler.js]
2019-06-21T11:43:26.274Z info [lib/handler.js]
The time difference between these 2 invocations was ~300ms. There were cases when such time was even 800ms. This doesn’t mean that chaincode processing is slow – it’s pretty fast both in Java and Node.js however probably some internal queueing causing this time gap which in our test scenario had the influence on the total result.
It’s also worth to notice that in both cases sometimes running 300 transactions in loop (this is how my testing script works) causes the “crash”. In production environment there are of course many ways to prevent it.
What to choose?
The primitive performance tests I did show that both chaincode implementations, Node.js and Java behave very similar. Of course when milliseconds are important – deeper tests should be done also for real life scenarios. There can be difference for example in large data set processing (big JSONs).
Deciding what technology too chose it’s important also to take into consideration some other Node.js vs Java differences.
For more complex domains I would stick with Java, since the statically typed language will be easier to maintain. Also testing for me is much simpler in Java than in Javascript. I appreciate also some of Java standards like JSR 303 which is bean validation, so validating complex domain in Java is easier for me (but I also have greater experience with Java than with JS).
Writing chaincode with JS can be easier, faster and as far as I noticed – more mature docs can be found for implementing chaincode in Fabric using Node.js than using Java.
Choosing the right technology is not easy and there are always some prons and cons. Think about the problem you want to solve, learn your domain and business logic then try to decide.