diff --git a/elements/lisk-chain/benchmark/calculate_diff.js b/elements/lisk-chain/benchmark/calculate_diff.js index 6504be053b0..6aed06e5187 100644 --- a/elements/lisk-chain/benchmark/calculate_diff.js +++ b/elements/lisk-chain/benchmark/calculate_diff.js @@ -55,8 +55,8 @@ const diff = calculateDiff( ); /** - * calculateDiff x 13.38 ops/sec ±1.96% (38 runs sampled) - * undo x 50,023 ops/sec ±0.52% (89 runs sampled) + * calculateDiff x 102,134 ops/sec ±0.99% (84 runs sampled) + * undo x 50,906 ops/sec ±0.80% (91 runs sampled) */ suite .add('calculateDiff', () => { diff --git a/elements/lisk-chain/src/diff.ts b/elements/lisk-chain/src/diff.ts index 0ee946ec22b..c7f15fbbb94 100644 --- a/elements/lisk-chain/src/diff.ts +++ b/elements/lisk-chain/src/diff.ts @@ -130,6 +130,75 @@ const diffAlgo = (initial: Buffer, final: Buffer): HistoryType[] => { return []; }; +/** + * This function returns the length of common prefix between two buffers + */ +const diffCommonPrefix = (buffer1: Buffer, buffer2: Buffer): number => { + // Quick check for common null cases. + if (buffer1[0] !== buffer2[0]) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + let pointerMin = 0; + let pointerMax = Math.min(buffer1.length, buffer2.length); + let pointerMid = pointerMax; + let pointerstart = 0; + + while (pointerMin < pointerMid) { + if ( + buffer1 + .slice(pointerstart, pointerMid) + .equals(buffer2.slice(pointerstart, pointerMid)) + ) { + pointerMin = pointerMid; + pointerstart = pointerMin; + } else { + pointerMax = pointerMid; + } + pointerMid = Math.floor((pointerMax - pointerMin) / 2 + pointerMin); + } + + return pointerMid; +}; + +/** + * This function returns the length of common suffix between two buffers + */ +const diffCommonSuffix = (buffer1: Buffer, buffer2: Buffer): number => { + // Quick check for common null cases. + if (buffer1[buffer1.length - 1] !== buffer2[buffer2.length - 1]) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + let pointerMin = 0; + let pointerMax = Math.min(buffer1.length, buffer2.length); + let pointerMid = pointerMax; + let pointerEnd = 0; + + while (pointerMin < pointerMid) { + if ( + buffer1 + .slice(buffer1.length - pointerMid, buffer1.length - pointerEnd) + .equals( + buffer2.slice( + buffer2.length - pointerMid, + buffer2.length - pointerEnd, + ), + ) + ) { + pointerMin = pointerMid; + pointerEnd = pointerMin; + } else { + pointerMax = pointerMid; + } + pointerMid = Math.floor((pointerMax - pointerMin) / 2 + pointerMin); + } + + return pointerMid; +}; + /** * This function reduces the diff generated by the diff algorithm based on unchanged bytes */ @@ -137,8 +206,30 @@ export const calculateDiff = ( initial: Buffer, final: Buffer, ): HistoryType[] => { - const longDiff = diffAlgo(initial, final); - const reducedDiff = []; + // When both the buffers are equal then return all '=' history + if (initial.equals(final)) { + return [['=', initial.length]]; + } + const commonPrefix = diffCommonPrefix(initial, final); + const strippedPrefixInitial = initial.slice(commonPrefix, initial.length); + const strippedPrefixFinal = final.slice(commonPrefix, final.length); + + const commonSuffix = diffCommonSuffix( + strippedPrefixInitial, + strippedPrefixFinal, + ); + const strippedInitial = strippedPrefixInitial.slice( + 0, + strippedPrefixInitial.length - commonSuffix, + ); + const strippedFinal = strippedPrefixFinal.slice( + 0, + strippedPrefixFinal.length - commonSuffix, + ); + const longDiff = diffAlgo(strippedInitial, strippedFinal); + + // Add common prefix to the reduced array in the start + const reducedDiff = commonPrefix > 0 ? [['=', commonPrefix]] : []; let count = 0; for (const b of longDiff) { @@ -152,9 +243,11 @@ export const calculateDiff = ( count += 1; } } - if (count > 0) { - reducedDiff.push(['=', count]); + // When commonSuffix or last counts are greater zero then combine them + if (count > 0 || commonSuffix > 0) { + reducedDiff.push(['=', count + commonSuffix]); } + return reducedDiff as HistoryType[]; }; diff --git a/elements/lisk-codec/README.md b/elements/lisk-codec/README.md index 0adebb7c432..ce2d2bdb187 100644 --- a/elements/lisk-codec/README.md +++ b/elements/lisk-codec/README.md @@ -8,6 +8,21 @@ $ npm install --save @liskhq/lisk-codec ``` +## Benchmarks + +The following are some benchmarks for version 0.1 of this library used for encoding and decoding different objects both generic and objects similar to the ones in the Lisk networks. + +Node version used: v12.17.0. Computer Spec: SSD, 6 Core, 16 GB RAM. No special configuration for Node. + +| Object Type | Encode (ops/sec) | Decode (ops/sec) | +| ------------- |:-------------:| -----:| +| Account | 75,081 | 86,908 | +| Transfer Transaction |225,229| 276,184 | +| Multi-signature registration (64 Members)| 23,539 | 44,231 | +| Block (15 KB payload)| 42,349 | 91,180 | + +This and additional benchmarks can be found in the `benchmarks` folder + ## License Copyright 2016-2019 Lisk Foundation diff --git a/elements/lisk-codec/benchmark/encode_decode_account.js b/elements/lisk-codec/benchmark/encode_decode_account.js new file mode 100644 index 00000000000..aca9c0d3da5 --- /dev/null +++ b/elements/lisk-codec/benchmark/encode_decode_account.js @@ -0,0 +1,177 @@ +/* + * Copyright © 2020 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +const { Suite } = require('benchmark'); +const { codec } = require('../dist-node/codec'); + +const suite = new Suite(); + +const account = { + address: Buffer.from('e11a11364738225813f86ea85214400e5db08d6e', 'hex'), + balance: BigInt(10), + publicKey: Buffer.from( + '0fd3c50a6d3bd17ea806c0566cf6cf10f6e3697d9bda1820b00cb14746bcccef', + 'hex', + ), + nonce: 5, + keys: { + numberOfSignatures: 2, + mandatoryKeys: [ + Buffer.from( + 'c8b8fbe474a2b63ccb9744a409569b0a465ee1803f80435aec1c5e7fc2d4ee18', + 'hex', + ), + Buffer.from( + '6115424fec0ce9c3bac5a81b5c782827d1f956fb95f1ccfa36c566d04e4d7267', + 'hex', + ), + ], + optionalKeys: [], + }, + asset: { + delegate: { + username: 'DelegateA', + pomHeights: [85], + consecutiveMissedBlocks: 32, + lastForgedHeight: 64, + isBanned: false, + totalVotesReceived: BigInt(300000000), + }, + sentVotes: [ + { + delegateAddress: Buffer.from( + 'cd32c73e9851c7137980063b8af64aa5a31651f8dcad258b682d2ddf091029e4', + 'hex', + ), + amount: BigInt(100000000), + }, + { + delegateAddress: Buffer.from( + '9d86ad24a3f030e5522b6598115bb4d70c1692c9d8995ddfccb377379a2d86c6', + 'hex', + ), + amount: BigInt(250000000), + }, + ], + unlocking: [ + { + delegateAddress: Buffer.from( + '655e665765e3c42712d9a425b5b720d10457a5e45de0d4420e7c53ad73b02ef5', + 'hex', + ), + amount: BigInt(400000000), + unvoteHeight: 128, + }, + ], + }, +}; + +const testSchema = { + $id: 'accountSchema', + type: 'object', + properties: { + address: { dataType: 'bytes', fieldNumber: 1 }, + balance: { dataType: 'uint64', fieldNumber: 2 }, + publicKey: { dataType: 'bytes', fieldNumber: 3 }, + nonce: { dataType: 'uint64', fieldNumber: 4 }, + keys: { + fieldNumber: 5, + type: 'object', + properties: { + numberOfSignatures: { dataType: 'uint32', fieldNumber: 1 }, + mandatoryKeys: { + type: 'array', + items: { dataType: 'bytes' }, + fieldNumber: 2, + }, + optionalKeys: { + type: 'array', + items: { dataType: 'bytes' }, + fieldNumber: 3, + }, + }, + required: ['numberOfSignatures', 'mandatoryKeys', 'optionalKeys'], + }, + asset: { + type: 'object', + fieldNumber: 6, + properties: { + delegate: { + type: 'object', + fieldNumber: 1, + properties: { + username: { dataType: 'string', fieldNumber: 1 }, + pomHeights: { + type: 'array', + items: { dataType: 'uint32' }, + fieldNumber: 2, + }, + consecutiveMissedBlocks: { dataType: 'uint32', fieldNumber: 3 }, + lastForgedHeight: { dataType: 'uint32', fieldNumber: 4 }, + isBanned: { dataType: 'boolean', fieldNumber: 5 }, + totalVotesReceived: { dataType: 'uint64', fieldNumber: 6 }, + }, + required: [ + 'username', + 'pomHeights', + 'consecutiveMissedBlocks', + 'lastForgedHeight', + 'isBanned', + 'totalVotesReceived', + ], + }, + sentVotes: { + type: 'array', + fieldNumber: 2, + items: { + type: 'object', + properties: { + delegateAddress: { dataType: 'bytes', fieldNumber: 1 }, + amount: { dataType: 'uint64', fieldNumber: 2 }, + }, + required: ['delegateAddress', 'amount'], + }, + }, + unlocking: { + type: 'array', + fieldNumber: 3, + items: { + type: 'object', + properties: { + delegateAddress: { dataType: 'bytes', fieldNumber: 1 }, + amount: { dataType: 'uint64', fieldNumber: 2 }, + unvoteHeight: { dataType: 'uint32', fieldNumber: 3 }, + }, + required: ['delegateAddress', 'amount', 'unvoteHeight'], + }, + }, + }, + }, + }, + required: ['address', 'balance', 'publicKey', 'nonce', 'keys', 'asset'], +}; + +const accountEncoded = codec.encode(testSchema, account); + +suite + .add('Encode Lisk account', () => { + codec.encode(testSchema, account); + }) + .add('Decode Lisk account', () => { + codec.decode(testSchema, accountEncoded); + }) + .on('cycle', function(event) { + console.log(String(event.target)); + }) + .run({ async: false }); diff --git a/elements/lisk-codec/benchmark/encode_decode_biggest_lisk_transaction.js b/elements/lisk-codec/benchmark/encode_decode_biggest_lisk_transaction.js new file mode 100644 index 00000000000..5f74dd58f3d --- /dev/null +++ b/elements/lisk-codec/benchmark/encode_decode_biggest_lisk_transaction.js @@ -0,0 +1,100 @@ +/* + * Copyright © 2020 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +const crypto = require('crypto'); + +const { Suite } = require('benchmark'); +const { codec } = require('../dist-node/codec'); + +const suite = new Suite(); + + +const mandatoryKeys = [...Array(44).keys()].map(() => crypto.randomBytes(32)); +const optionalKeys = [...Array(20).keys()].map(() => crypto.randomBytes(32)); +const signatures = [...Array(65).keys()].map(() => crypto.randomBytes(64)); + + +const biggestMultisigTransactionRegistration = { + senderPublicKey: Buffer.from( + '0b211fce4b615083701cb8a8c99407e464b2f9aa4f367095322de1b77e5fcfbe', + 'hex', + ), + nonce: 1, + fee: BigInt(1500000000), + type: 12, + asset: { + mandatoryKeys, + optionalKeys, + numberOfSignatures: 44, + }, + signatures, +}; + +const testSchema = { + $id: 'testSchema', + type: 'object', + properties: { + senderPublicKey: { fieldNumber: 1, dataType: 'bytes' }, + nonce: { fieldNumber: 2, dataType: 'uint32' }, + free: { fieldNumber: 3, dataType: 'uint64' }, + type: { fieldNumber: 4, dataType: 'uint32' }, + asset: { + type: 'object', + fieldNumber: 5, + properties: { + numberOfSignatures: { + dataType: 'uint32', + fieldNumber: 1, + }, + mandatoryKeys: { + fieldNumber: 2, + type: 'array', + items: { + dataType: 'bytes', + }, + }, + optionalKeys: { + fieldNumber: 3, + type: 'array', + items: { + dataType: 'bytes', + }, + }, + }, + }, + signatures: { + fieldNumber: 6, + type: 'array', + items: { + dataType: 'bytes', + }, + }, + }, +}; + +const biggestMultisigTransactionRegistrationEncoded = codec.encode( + testSchema, + biggestMultisigTransactionRegistration, +); + +suite + .add('Encode biggest possible Lisk transaction', () => { + codec.encode(testSchema, biggestMultisigTransactionRegistration); + }) + .add('Decode biggest possible Lisk transaction', () => { + codec.decode(testSchema, biggestMultisigTransactionRegistrationEncoded); + }) + .on('cycle', function(event) { + console.log(String(event.target)); + }) + .run({ async: false }); diff --git a/elements/lisk-codec/benchmark/encode_decode_full_block.js b/elements/lisk-codec/benchmark/encode_decode_full_block.js new file mode 100644 index 00000000000..9b87007dd2b --- /dev/null +++ b/elements/lisk-codec/benchmark/encode_decode_full_block.js @@ -0,0 +1,53 @@ +/* + * Copyright © 2020 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +const crypto = require('crypto'); + +const { Suite } = require('benchmark'); +const { codec } = require('../dist-node/codec'); + +const payload = [...Array(65).keys()].map(() => crypto.randomBytes(220)); + +const suite = new Suite(); + +const block = { + header: Buffer.from( + 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', + 'hex', + ), + payload, +}; + +const testSchema = { + $id: 'blockSchema', + type: 'object', + properties: { + header: { dataType: 'bytes', fieldNumber: 1 }, + payload: { type: 'array', items: { dataType: 'bytes' }, fieldNumber: 2 }, + }, + required: ['header', 'payload'], +}; + +const blockEncoded = codec.encode(testSchema, block); + +suite + .add('Encode Lisk block', () => { + codec.encode(testSchema, block); + }) + .add('Decode Lisk block', () => { + codec.decode(testSchema, blockEncoded); + }) + .on('cycle', function(event) { + console.log(String(event.target)); + }) + .run({ async: false }); diff --git a/elements/lisk-codec/benchmark/encode_decode_small_lisk_transaction.js b/elements/lisk-codec/benchmark/encode_decode_small_lisk_transaction.js new file mode 100644 index 00000000000..feb628b9623 --- /dev/null +++ b/elements/lisk-codec/benchmark/encode_decode_small_lisk_transaction.js @@ -0,0 +1,99 @@ +/* + * Copyright © 2020 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +const { Suite } = require('benchmark'); +const { codec } = require('../dist-node/codec'); + +const suite = new Suite(); + +const transferLikeLiskTransaction = { + senderPublicKey: Buffer.from( + '0b211fce4b615083701cb8a8c99407e464b2f9aa4f367095322de1b77e5fcfbe', + 'hex', + ), + nonce: 1, + fee: BigInt(1500000000), + type: 8, + asset: { + amount: BigInt(15000000000), + recipientAddress: Buffer.from( + '0b811fce4b615883709cb8a8c99407e464b2f9aa4f367095322de1b87g5fdfb1', + 'hex', + ), + data: 'Test data', + }, + signatures: [ + Buffer.from( + '4d38666425327e3c950cef3d5d6bed86b7a32e32002651a49ed5dbd0143d9b2fe94d1aa970ff6492da8e174f844d3c4736f980b322d35b76903969c48375ad8a', + 'hex', + ), + Buffer.from( + '644abb27920144bb4e4a5030e77cbdc53ea03fb9dd4ecd9abb8a5653c581f766b4ca6f33ce5f603330959e2f8263ae187c35b1840a840107cb42899bf854db01', + 'hex', + ), + ], +}; + +const testSchema = { + $id: 'testSchema', + type: 'object', + properties: { + senderPublicKey: { fieldNumber: 1, dataType: 'bytes' }, + nonce: { fieldNumber: 2, dataType: 'uint32' }, + free: { fieldNumber: 3, dataType: 'uint64' }, + type: { fieldNumber: 4, dataType: 'uint32' }, + asset: { + type: 'object', + fieldNumber: 5, + properties: { + amount: { + fieldNumber: 1, + dataType: 'uint64', + }, + recipientAddress: { + fieldNumber: 2, + dataType: 'bytes', + }, + data: { + fieldNumber: 3, + dataType: 'string', + }, + }, + }, + signatures: { + fieldNumber: 6, + type: 'array', + items: { + dataType: 'bytes', + }, + }, + }, +}; + +const transferLikeLiskTransactionEncoded = codec.encode( + testSchema, + transferLikeLiskTransaction, +); + +suite + .add('Encode transfer like Lisk transaction', () => { + codec.encode(testSchema, transferLikeLiskTransaction); + }) + .add('Decode transfer like Lisk transaction', () => { + codec.decode(testSchema, transferLikeLiskTransactionEncoded); + }) + .on('cycle', function(event) { + console.log(String(event.target)); + }) + .run({ async: false });