diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b96a9ac..de61afc2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,3 +38,4 @@ jobs: run: test -z "$(git status --porcelain)" - name: Run tests run: pnpm test + if: matrix.os == 'ubuntu-latest' diff --git a/benchmark/.gitignore b/benchmark/.gitignore new file mode 100644 index 00000000..c795b054 --- /dev/null +++ b/benchmark/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 00000000..a17199dc --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,457 @@ +# Gas and size benchmark compare to NEAR-SDK-RS + +## Summary + +NEAR-SDK-JS bundles a bytecode VM with the contract bytecode to a wasm file. Currently, the bytecode VM is the QuickJS runtime with interface to NEAR and the contract bytecode is compiled from JavaScript source code with QuickJS Compiler (QJSC). + +This results in: + +- Size of a minimal contract is 500K, which is also the size of the bytecode VM. +- Bytecode is more compact than wasm. Complex contract in JS adds less bytes to the equivalent wasm compiled from Rust, but due to the initial 500K size, the result contract is still bigger and within same order of magnitude: several hundred KB. +- For contract that bottlenecks at calling the host functions are using similar gas in JS and Rust. +- For contract that does a lot of computation in JS, the JS bytecode uses significantly more gas. +- For a real world contract, if it doesn't including complex logic in JavaScript, it's usually sufficient, consider the complexity of the near contract standards. +- For more complex logic, We suggest to bench the most expensive contract call, including most complex path of cross contract calls, to determine whether it fits 300T Gas limit. + +## Detailed gas benchmark + +### A minimal contract + +- RS lowlevel minimal contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 2.43T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.00005T + - Gas used to refund unused gas: 0.22318T + - Total gas used: 5.08T +- JS lowlevel minimal contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 7.07T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.11132T + - WASM_INSTRUCTION : 4.53T + - Gas used to refund unused gas: 0.22318T + - Total gas used: 9.72T + +In the very minimal contract the JS adds about `1.8T` gas. The major difference is loading the QuickJS VM and near-sdk-js uses 4.53T Gas. The 500K contract loading just adds 0.1T Gas. + +### A highlevel minimal contract (using nearbindgen) + +- highlevel-minimal.ava › RS highlevel minimal contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 2.63T + - BASE : 0.79G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 35.46G + - READ_CACHED_TRIE_NODE : 4.56G + - READ_MEMORY_BASE : 7.83G + - READ_MEMORY_BYTE : 0.04G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - TOUCHING_TRIE_NODE : 32.2G + - WASM_INSTRUCTION : 0.46G + - WRITE_MEMORY_BASE : 2.8G + - WRITE_MEMORY_BYTE : 0.04G + - Gas used to refund unused gas: 223.18G + - Total gas used: 5.28T +- highlevel-minimal.ava › JS highlevel minimal contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 8.39T + - BASE : 1.59G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 112.03G + - READ_CACHED_TRIE_NODE : 6.84G + - READ_MEMORY_BASE : 7.83G + - READ_MEMORY_BYTE : 0.05G + - READ_REGISTER_BASE : 2.52G + - READ_REGISTER_BYTE : 0G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - STORAGE_WRITE_VALUE_BYTE : 0.06G + - TOUCHING_TRIE_NODE : 48.31G + - WASM_INSTRUCTION : 5.66T + - WRITE_MEMORY_BASE : 5.61G + - WRITE_MEMORY_BYTE : 0.05G + - WRITE_REGISTER_BASE : 2.87G + - WRITE_REGISTER_BYTE : 0.01G + - Gas used to refund unused gas: 223.18G + - Total gas used: 11.05T + +JS `@NearBindgen` is more expensive, the major difference is in `WASM_INSTRUCTION`, because `@NearBindgen` does some class, object manipulation work, but Rust `near_bindgen` is a compile time code generation macro. Deduct the 4.5T loading VM and near-sdk-js, it's about 1T gas overhead. + +### Low level API + +- RS lowlevel API contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 2.53T + - BASE : 0.00026T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.00008T + - READ_MEMORY_BASE : 0.00522T + - READ_MEMORY_BYTE : 0.00008T + - STORAGE_WRITE_BASE : 0.0642T + - STORAGE_WRITE_KEY_BYTE : 0.0007T + - STORAGE_WRITE_VALUE_BYTE : 0.00031T + - TOUCHING_TRIE_NODE : 0.0322T + - WASM_INSTRUCTION : 0.00002T + - Gas used to refund unused gas: 0.22318T + - Total gas used: 5.18T +- JS lowlevel API contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 7.8T + - BASE : 0.00026T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.11119T + - READ_MEMORY_BASE : 0.00522T + - READ_MEMORY_BYTE : 0.00008T + - STORAGE_WRITE_BASE : 0.0642T + - STORAGE_WRITE_EVICTED_BYTE : 0.00032T + - STORAGE_WRITE_KEY_BYTE : 0.0007T + - STORAGE_WRITE_VALUE_BYTE : 0.00031T + - TOUCHING_TRIE_NODE : 0.09661T + - WASM_INSTRUCTION : 5.09T + - WRITE_REGISTER_BASE : 0.00287T + - WRITE_REGISTER_BYTE : 0.00004T + - Gas used to refund unused gas: 0.22318T + - Total gas used: 10.45T +- JS lowlevel API contract, call many + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 8.47T + - BASE : 0.00265T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.11119T + - READ_MEMORY_BASE : 0.0522T + - READ_MEMORY_BYTE : 0.00076T + - STORAGE_WRITE_BASE : 0.64197T + - STORAGE_WRITE_EVICTED_BYTE : 0.00289T + - STORAGE_WRITE_KEY_BYTE : 0.00705T + - STORAGE_WRITE_VALUE_BYTE : 0.0031T + - TOUCHING_TRIE_NODE : 0.04831T + - WASM_INSTRUCTION : 5.14T + - WRITE_REGISTER_BASE : 0.02579T + - WRITE_REGISTER_BYTE : 0.00034T + - Gas used to refund unused gas: 0.22318T + - Total gas used: 11.12T + +In this case, JS lowlevel API contract uses same gas in the storage write API part (`STORAGE_WRITE_BASE` / `STORAGE_WRITE_KEY_BYTE` / `STORAGE_WRITE_VALUE_BYTE` ). The major excessive gas is due to the overhead of initialize QuickJS VM and loading near-sdk-js. We can see this more obviously by calling storage write for 10 times ("call many tests" in above). + +### Highlevel collection + +- RS highlevel collection contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 3.32T + - BASE : 3.18G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 70.94G + - READ_CACHED_TRIE_NODE : 95.76G + - READ_MEMORY_BASE : 26.1G + - READ_MEMORY_BYTE : 1.87G + - READ_REGISTER_BASE : 5.03G + - READ_REGISTER_BYTE : 0.03G + - STORAGE_READ_BASE : 112.71G + - STORAGE_READ_KEY_BYTE : 3.44G + - STORAGE_READ_VALUE_BYTE : 0.19G + - STORAGE_WRITE_BASE : 256.79G + - STORAGE_WRITE_EVICTED_BYTE : 1.09G + - STORAGE_WRITE_KEY_BYTE : 9.23G + - STORAGE_WRITE_VALUE_BYTE : 7.75G + - TOUCHING_TRIE_NODE : 257.63G + - WASM_INSTRUCTION : 16.36G + - WRITE_MEMORY_BASE : 8.41G + - WRITE_MEMORY_BYTE : 0.74G + - WRITE_REGISTER_BASE : 8.6G + - WRITE_REGISTER_BYTE : 1.1G + - Gas used to refund unused gas: 223.18G + - Total gas used: 5.97T +- JS highlevel collection contract + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 10.06T + - BASE : 2.91G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 113.46G + - READ_CACHED_TRIE_NODE : 72.96G + - READ_MEMORY_BASE : 20.88G + - READ_MEMORY_BYTE : 2G + - READ_REGISTER_BASE : 5.03G + - READ_REGISTER_BYTE : 0.03G + - STORAGE_READ_BASE : 112.71G + - STORAGE_READ_KEY_BYTE : 3.31G + - STORAGE_READ_VALUE_BYTE : 0.53G + - STORAGE_WRITE_BASE : 192.59G + - STORAGE_WRITE_EVICTED_BYTE : 3.02G + - STORAGE_WRITE_KEY_BYTE : 7.96G + - STORAGE_WRITE_VALUE_BYTE : 9.49G + - TOUCHING_TRIE_NODE : 209.33G + - WASM_INSTRUCTION : 6.86T + - WRITE_MEMORY_BASE : 8.41G + - WRITE_MEMORY_BYTE : 0.9G + - WRITE_REGISTER_BASE : 8.6G + - WRITE_REGISTER_BYTE : 1.55G + - Gas used to refund unused gas: 223.18G + - Total gas used: 12.71T + +JS SDK's collection has about 1T overhead, deduct the 4.5T VM/near-sdk-js loading and 1T `@NearBindgen`. Note this benches the most complicated `UnorderedMap`, which gas usage is strictly greater than the other collections. And the gas used in actual writing the collection to storage is similar (`STORAGE_WRITE_BASE` / `STORAGE_WRITE_KEY_BYTE` / `STORAGE_WRITE_VALUE_BYTE` ). + +### Computational expensive contract + +- JS expensive contract, iterate 20000 times + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 123.26T + - BASE : 1.85G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 112.09G + - READ_CACHED_TRIE_NODE : 4.56G + - READ_MEMORY_BASE : 10.44G + - READ_MEMORY_BYTE : 0.07G + - READ_REGISTER_BASE : 2.52G + - READ_REGISTER_BYTE : 0G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - STORAGE_WRITE_VALUE_BYTE : 0.06G + - TOUCHING_TRIE_NODE : 32.2G + - WASM_INSTRUCTION : 120.54T + - WRITE_MEMORY_BASE : 5.61G + - WRITE_MEMORY_BYTE : 0.07G + - WRITE_REGISTER_BASE : 2.87G + - WRITE_REGISTER_BYTE : 0.04G + - Gas used to refund unused gas: 223.18G + - Total gas used: 125.91T +- RS expensive contract. iterate 20000 times + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 3.01T + - BASE : 1.85G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 67.77G + - READ_CACHED_TRIE_NODE : 6.84G + - READ_MEMORY_BASE : 10.44G + - READ_MEMORY_BYTE : 0.06G + - READ_REGISTER_BASE : 2.52G + - READ_REGISTER_BYTE : 0G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - TOUCHING_TRIE_NODE : 48.31G + - WASM_INSTRUCTION : 315.17G + - WRITE_MEMORY_BASE : 5.61G + - WRITE_MEMORY_BYTE : 0.07G + - WRITE_REGISTER_BASE : 2.87G + - WRITE_REGISTER_BYTE : 0.04G + - Gas used to refund unused gas: 223.18G + - Total gas used: 5.66T +- RS expensive contract. iterate 10000 times + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 2.9T + - BASE : 2.38G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 67.77G + - READ_CACHED_TRIE_NODE : 13.68G + - READ_MEMORY_BASE : 10.44G + - READ_MEMORY_BYTE : 0.06G + - READ_REGISTER_BASE : 5.03G + - READ_REGISTER_BYTE : 0G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - TOUCHING_TRIE_NODE : 80.51G + - WASM_INSTRUCTION : 158.89G + - WRITE_MEMORY_BASE : 8.41G + - WRITE_MEMORY_BYTE : 0.07G + - WRITE_REGISTER_BASE : 8.6G + - WRITE_REGISTER_BYTE : 0.04G + - Gas used to refund unused gas: 223.18G + - Total gas used: 5.56T +- RS expensive contract. iterate 100 times + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 2.75T + - BASE : 2.38G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 67.77G + - READ_CACHED_TRIE_NODE : 13.68G + - READ_MEMORY_BASE : 10.44G + - READ_MEMORY_BYTE : 0.05G + - READ_REGISTER_BASE : 5.03G + - READ_REGISTER_BYTE : 0G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - TOUCHING_TRIE_NODE : 80.51G + - WASM_INSTRUCTION : 4.02G + - WRITE_MEMORY_BASE : 8.41G + - WRITE_MEMORY_BYTE : 0.07G + - WRITE_REGISTER_BASE : 8.6G + - WRITE_REGISTER_BYTE : 0.03G + - Gas used to refund unused gas: 223.18G + - Total gas used: 5.4T +- JS expensive contract, iterate 100 times + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 9.09T + - BASE : 2.38G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 112.09G + - READ_CACHED_TRIE_NODE : 13.68G + - READ_MEMORY_BASE : 10.44G + - READ_MEMORY_BYTE : 0.06G + - READ_REGISTER_BASE : 5.03G + - READ_REGISTER_BYTE : 0G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_READ_VALUE_BYTE : 0.01G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_EVICTED_BYTE : 0.06G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - STORAGE_WRITE_VALUE_BYTE : 0.06G + - TOUCHING_TRIE_NODE : 80.51G + - WASM_INSTRUCTION : 6.3T + - WRITE_MEMORY_BASE : 8.41G + - WRITE_MEMORY_BYTE : 0.07G + - WRITE_REGISTER_BASE : 8.6G + - WRITE_REGISTER_BYTE : 0.05G + - Gas used to refund unused gas: 223.18G + - Total gas used: 11.75T +- JS expensive contract, iterate 10000 times + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 65.94T + - BASE : 2.38G + - CONTRACT_LOADING_BASE : 0.04G + - CONTRACT_LOADING_BYTES : 112.09G + - READ_CACHED_TRIE_NODE : 13.68G + - READ_MEMORY_BASE : 10.44G + - READ_MEMORY_BYTE : 0.06G + - READ_REGISTER_BASE : 5.03G + - READ_REGISTER_BYTE : 0G + - STORAGE_READ_BASE : 56.36G + - STORAGE_READ_KEY_BYTE : 0.15G + - STORAGE_READ_VALUE_BYTE : 0.01G + - STORAGE_WRITE_BASE : 64.2G + - STORAGE_WRITE_EVICTED_BYTE : 0.06G + - STORAGE_WRITE_KEY_BYTE : 0.35G + - STORAGE_WRITE_VALUE_BYTE : 0.06G + - TOUCHING_TRIE_NODE : 80.51G + - WASM_INSTRUCTION : 63.15T + - WRITE_MEMORY_BASE : 8.41G + - WRITE_MEMORY_BYTE : 0.08G + - WRITE_REGISTER_BASE : 8.6G + - WRITE_REGISTER_BYTE : 0.06G + - Gas used to refund unused gas: 223.18G + - Total gas used: 68.59T + +In this case, JS uses much more gas. Because JS Number is object and that's a lot of overhead compare to native integer arithmetic. It's even a lot of overhead compare to native float arithmetic. Also in QuickJS there's no JIT. If your contract does a lot of calculation or complex algorithm in JavaScript, it'd be better to do a similar benchmark. + +### Deploy and cross contract call + +- JS promise batch deploy contract and call + + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 25.86T + - CREATE_ACCOUNT : 0.09961T + - DEPLOY_CONTRACT : 3.71T + - FUNCTION_CALL : 2.32T + - NEW_RECEIPT : 0.10806T + - TRANSFER : 0.11512T + - BASE : 0.00159T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.22386T + - PROMISE_RETURN : 0.00056T + - READ_MEMORY_BASE : 0.01566T + - READ_MEMORY_BYTE : 1.97T + - UTF8_DECODING_BASE : 0.00311T + - UTF8_DECODING_BYTE : 0.00525T + - WASM_INSTRUCTION : 14.86T + - Gas used to execute the cross contract call: 41.9T + - BASE : 0.00344T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.11228T + - READ_MEMORY_BASE : 0.00261T + - READ_MEMORY_BYTE : 0.0005T + - READ_REGISTER_BASE : 0.01007T + - READ_REGISTER_BYTE : 0T + - WASM_INSTRUCTION : 5.47T + - WRITE_MEMORY_BASE : 0.01122T + - WRITE_MEMORY_BYTE : 0.00014T + - WRITE_REGISTER_BASE : 0.01146T + - WRITE_REGISTER_BYTE : 0.00019T + - Gas used to refund unused gas for cross contract call: 0.22318T + - Gas used to refund unused gas: 0.22318T + - Total gas used: 70.63T + +- RS promise batch deploy contract and call + - Gas used to convert transaction to receipt: 2.43T + - Gas used to execute the receipt (actual contract call): 10.89T + - CREATE_ACCOUNT : 0.09961T + - DEPLOY_CONTRACT : 3.71T + - FUNCTION_CALL : 2.32T + - NEW_RECEIPT : 0.10806T + - TRANSFER : 0.11512T + - BASE : 0.00159T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.11283T + - PROMISE_RETURN : 0.00056T + - READ_MEMORY_BASE : 0.01566T + - READ_MEMORY_BYTE : 1.97T + - UTF8_DECODING_BASE : 0.00311T + - UTF8_DECODING_BYTE : 0.00525T + - WASM_INSTRUCTION : 0.00038T + - Gas used to execute the cross contract call: 41.9T + - BASE : 0.00344T + - CONTRACT_LOADING_BASE : 0.00004T + - CONTRACT_LOADING_BYTES : 0.11228T + - READ_MEMORY_BASE : 0.00261T + - READ_MEMORY_BYTE : 0.0005T + - READ_REGISTER_BASE : 0.01007T + - READ_REGISTER_BYTE : 0T + - WASM_INSTRUCTION : 5.47T + - WRITE_MEMORY_BASE : 0.01122T + - WRITE_MEMORY_BYTE : 0.00014T + - WRITE_REGISTER_BASE : 0.01146T + - WRITE_REGISTER_BYTE : 0.00019T + - Gas used to refund unused gas for cross contract call: 0.22318T + - Gas used to refund unused gas: 0.22318T + - Total gas used: 55.67T + +In this test, we use a JS contract and RS contract to both deploy a JS contract and cross contract call this newly deployed contract. We can see the gas to do the cross contract call is the same. JS SDK has a `~10T` overhead to parse a `~500K` contract in byte. This is because JS, either represent code in Uint8Array or string has some overhead while rust compiler can directly turn it into data section in wasm. In practice, a 10T overhead for a one time contract deploy is not a big deal. + +## Tips to do your own benchmark + +If the above cases don't cover use case or you have a complex algorithm to implement in JavaScript, it's a good idea to benchmark your specific algorithm before choose near-sdk-js for your project. + +You don't have to implement the exact algorithm to estimate the gas usage. Instead, you can find out the most expensive execution path of the algorithm, and estimate it by using the upper bound. For example, store the biggest possible objects into the collection and iterate for most possible times. Then goes to write the benchmark and the total gas cannot be more than 300T to be a valid contract. Also, if it has cross contract call, make sure the total gas, that's a sum of all cross contract calls, is less than 300T. + +To add your benchmark, write a one function contract of your most expensive operation. And write a test to call this function. If it doesn't involve cross contract call or promises, creating such test is simple. You can refer to `bench/src/expensive-calc.js` and `bench/__tests__/test-expensive-calc.ava.js` on how to write such test and print the gas breakdown. If it involves create promises or cross contract calls, printing the gas breakdown is a little bit more complex, you can refer to `bench/__tests__/test-deploy-contract.ava.js` for the recipe. + +## Details of size benchmark + +### JS Contract + +``` + +-rwxrwxr-x 1 bo bo 1009K Feb 9 10:49 ./build/deploy-contract.wasm +-rwxrwxr-x 1 bo bo 506K Feb 8 12:11 ./build/expensive-calc.wasm +-rwxrwxr-x 1 bo bo 512K Feb 7 15:57 ./build/highlevel-collection.wasm +-rwxrwxr-x 1 bo bo 505K Feb 7 10:53 ./build/highlevel-minimal.wasm +-rwxrwxr-x 1 bo bo 502K Feb 10 11:32 ./build/lowlevel-api.wasm +-rwxrwxr-x 1 bo bo 502K Feb 10 11:47 ./build/lowlevel-minimal.wasm +``` + +### Rust Contract + +``` +-rwxrwxr-x 1 bo bo 509K Feb 10 10:02 ./res/deploy_contract.wasm +-rwxrwxr-x 1 bo bo 306K Feb 8 12:18 ./res/expensive_calc.wasm +-rwxrwxr-x 1 bo bo 320K Feb 8 11:26 ./res/highlevel_collection.wasm +-rwxrwxr-x 1 bo bo 160K Feb 7 10:51 ./res/highlevel_minimal.wasm +-rwxrwxr-x 1 bo bo 387 Feb 7 11:56 ./res/lowlevel_api.wasm +-rwxrwxr-x 1 bo bo 219 Feb 7 10:33 ./res/lowlevel_minimal.wasm +``` + +## Appendix + +- Source code of the rust benchmark: https://github.com/near/sdk-rs-gas-benchmark diff --git a/benchmark/__tests__/test-deploy-contract.ava.js b/benchmark/__tests__/test-deploy-contract.ava.js new file mode 100644 index 00000000..cbbafffc --- /dev/null +++ b/benchmark/__tests__/test-deploy-contract.ava.js @@ -0,0 +1,144 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; +import { + formatGas, + gasBreakdown, + logGasBreakdown, + logGasDetail, +} from "./util.js"; + +test.before(async (t) => { + // Init the worker and start a Sandbox server + const worker = await Worker.init(); + + // Prepare sandbox for tests, create accounts, deploy contracts, etx. + const root = worker.rootAccount; + + // Deploy the test contract. + const callerContract = await root.createSubAccount("caller", { + initialBalance: "1000N", + }); + await callerContract.deploy("build/deploy-contract.wasm"); + + const callerContractRs = await root.createSubAccount("callrs", { + initialBalance: "1000N", + }); + await callerContractRs.deploy("res/deploy_contract.wasm"); + // Test users + const ali = await root.createSubAccount("ali"); + const bob = await root.createSubAccount("bob"); + const carl = await root.createSubAccount("carl"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { + root, + callerContract, + ali, + bob, + carl, + callerContractRs, + }; +}); + +test("JS promise batch deploy contract and call", async (t) => { + const { bob, callerContract } = t.context.accounts; + + let r = await bob.callRaw(callerContract, "deploy_contract", "", { + gas: "300 Tgas", + }); + // console.log(JSON.stringify(r, null, 2)); + let deployed = callerContract.getSubAccount("a"); + t.deepEqual(JSON.parse(Buffer.from(r.result.status.SuccessValue, "base64")), { + currentAccountId: deployed.accountId, + signerAccountId: bob.accountId, + predecessorAccountId: callerContract.accountId, + input: "abc", + }); + + t.log( + "Gas used to convert transaction to receipt: ", + formatGas(r.result.transaction_outcome.outcome.gas_burnt) + ); + t.log( + "Gas used to execute the receipt (actual contract call): ", + formatGas(r.result.receipts_outcome[0].outcome.gas_burnt) + ); + let map = gasBreakdown(r.result.receipts_outcome[0].outcome); + logGasBreakdown(map, t); + t.log( + "Gas used to execute the cross contract call: ", + formatGas(r.result.receipts_outcome[1].outcome.gas_burnt) + ); + map = gasBreakdown(r.result.receipts_outcome[1].outcome); + logGasBreakdown(map, t); + t.log( + "Gas used to refund unused gas for cross contract call: ", + formatGas(r.result.receipts_outcome[2].outcome.gas_burnt) + ); + t.log( + "Gas used to refund unused gas: ", + formatGas(r.result.receipts_outcome[3].outcome.gas_burnt) + ); + t.log( + "Total gas used: ", + formatGas( + r.result.transaction_outcome.outcome.gas_burnt + + r.result.receipts_outcome[0].outcome.gas_burnt + + r.result.receipts_outcome[1].outcome.gas_burnt + + r.result.receipts_outcome[2].outcome.gas_burnt + + r.result.receipts_outcome[3].outcome.gas_burnt + ) + ); +}); + +test("RS promise batch deploy contract and call", async (t) => { + const { bob, callerContractRs } = t.context.accounts; + + let r = await bob.callRaw(callerContractRs, "deploy_contract", "", { + gas: "300 Tgas", + }); + // console.log(JSON.stringify(r, null, 2)); + let deployed = callerContractRs.getSubAccount("a"); + t.deepEqual(JSON.parse(Buffer.from(r.result.status.SuccessValue, "base64")), { + currentAccountId: deployed.accountId, + signerAccountId: bob.accountId, + predecessorAccountId: callerContractRs.accountId, + input: "abc", + }); + + t.log( + "Gas used to convert transaction to receipt: ", + formatGas(r.result.transaction_outcome.outcome.gas_burnt) + ); + t.log( + "Gas used to execute the receipt (actual contract call): ", + formatGas(r.result.receipts_outcome[0].outcome.gas_burnt) + ); + let map = gasBreakdown(r.result.receipts_outcome[0].outcome); + logGasBreakdown(map, t); + t.log( + "Gas used to execute the cross contract call: ", + formatGas(r.result.receipts_outcome[1].outcome.gas_burnt) + ); + map = gasBreakdown(r.result.receipts_outcome[1].outcome); + logGasBreakdown(map, t); + t.log( + "Gas used to refund unused gas for cross contract call: ", + formatGas(r.result.receipts_outcome[2].outcome.gas_burnt) + ); + t.log( + "Gas used to refund unused gas: ", + formatGas(r.result.receipts_outcome[3].outcome.gas_burnt) + ); + t.log( + "Total gas used: ", + formatGas( + r.result.transaction_outcome.outcome.gas_burnt + + r.result.receipts_outcome[0].outcome.gas_burnt + + r.result.receipts_outcome[1].outcome.gas_burnt + + r.result.receipts_outcome[2].outcome.gas_burnt + + r.result.receipts_outcome[3].outcome.gas_burnt + ) + ); +}); diff --git a/benchmark/__tests__/test-expensive-calc.ava.js b/benchmark/__tests__/test-expensive-calc.ava.js new file mode 100644 index 00000000..5743227f --- /dev/null +++ b/benchmark/__tests__/test-expensive-calc.ava.js @@ -0,0 +1,86 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; +import { logGasDetail } from "./util.js"; + +test.before(async (t) => { + // Init the worker and start a Sandbox server + const worker = await Worker.init(); + + // Prepare sandbox for tests, create accounts, deploy contracts, etx. + const root = worker.rootAccount; + + // Deploy the test contract. + const expensiveContract = await root.devDeploy("build/expensive-calc.wasm"); + const expensiveContractRs = await root.devDeploy("res/expensive_calc.wasm"); + + // Test users + const ali = await root.createSubAccount("ali"); + const bob = await root.createSubAccount("bob"); + const carl = await root.createSubAccount("carl"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { + root, + expensiveContract, + expensiveContractRs, + ali, + bob, + carl, + }; +}); + +test("JS expensive contract, iterate 100 times", async (t) => { + const { bob, expensiveContract } = t.context.accounts; + let r = await bob.callRaw(expensiveContract, "expensive", { n: 100 }); + + t.is(r.result.status.SuccessValue, "LTUw"); + logGasDetail(r, t); +}); + +test("RS expensive contract. iterate 100 times", async (t) => { + const { bob, expensiveContractRs } = t.context.accounts; + let r = await bob.callRaw(expensiveContractRs, "expensive", { n: 100 }); + t.is(r.result.status.SuccessValue, "LTUw"); + logGasDetail(r, t); +}); + +test("JS expensive contract, iterate 10000 times", async (t) => { + const { bob, expensiveContract } = t.context.accounts; + let r = await bob.callRaw( + expensiveContract, + "expensive", + { n: 10000 }, + { gas: BigInt(300 * 10 ** 12) } + ); + + t.is(r.result.status.SuccessValue, "LTUwMDA="); + logGasDetail(r, t); +}); + +test("RS expensive contract. iterate 10000 times", async (t) => { + const { bob, expensiveContractRs } = t.context.accounts; + let r = await bob.callRaw(expensiveContractRs, "expensive", { n: 10000 }); + t.is(r.result.status.SuccessValue, "LTUwMDA="); + logGasDetail(r, t); +}); + +test("JS expensive contract, iterate 20000 times", async (t) => { + const { bob, expensiveContract } = t.context.accounts; + let r = await bob.callRaw( + expensiveContract, + "expensive", + { n: 20000 }, + { gas: BigInt(300 * 10 ** 12) } + ); + + t.is(r.result.status.SuccessValue, "LTEwMDAw"); + logGasDetail(r, t); +}); + +test("RS expensive contract. iterate 20000 times", async (t) => { + const { bob, expensiveContractRs } = t.context.accounts; + let r = await bob.callRaw(expensiveContractRs, "expensive", { n: 20000 }); + t.is(r.result.status.SuccessValue, "LTEwMDAw"); + logGasDetail(r, t); +}); diff --git a/benchmark/__tests__/test-highlevel-collection.ava.js b/benchmark/__tests__/test-highlevel-collection.ava.js new file mode 100644 index 00000000..70c74d3e --- /dev/null +++ b/benchmark/__tests__/test-highlevel-collection.ava.js @@ -0,0 +1,72 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; +import { logGasDetail } from "./util.js"; + +test.before(async (t) => { + // Init the worker and start a Sandbox server + const worker = await Worker.init(); + + // Prepare sandbox for tests, create accounts, deploy contracts, etx. + const root = worker.rootAccount; + + // Deploy the test contract. + const highlevelContract = await root.devDeploy( + "build/highlevel-collection.wasm" + ); + const highlevelContractRs = await root.devDeploy( + "res/highlevel_collection.wasm" + ); + + // Test users + const ali = await root.createSubAccount("ali"); + const bob = await root.createSubAccount("bob"); + const carl = await root.createSubAccount("carl"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { + root, + highlevelContract, + highlevelContractRs, + ali, + bob, + carl, + }; +}); + +test("JS highlevel collection contract", async (t) => { + const { bob, highlevelContract } = t.context.accounts; + let r = await bob.callRaw(highlevelContract, "set", { + key: "a".repeat(100), + value: "b".repeat(100), + }); + r = await bob.callRaw(highlevelContract, "set", { + key: "b".repeat(100), + value: "c".repeat(100), + }); + r = await bob.callRaw(highlevelContract, "set", { + key: "c".repeat(100), + value: "d".repeat(100), + }); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); + +test("RS highlevel collection contract", async (t) => { + const { bob, highlevelContractRs } = t.context.accounts; + let r = await bob.callRaw(highlevelContractRs, "set", { + key: "a".repeat(100), + value: "b".repeat(100), + }); + r = await bob.callRaw(highlevelContractRs, "set", { + key: "b".repeat(100), + value: "c".repeat(100), + }); + r = await bob.callRaw(highlevelContractRs, "set", { + key: "c".repeat(100), + value: "d".repeat(100), + }); + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); diff --git a/benchmark/__tests__/test-highlevel-minimal.ava.js b/benchmark/__tests__/test-highlevel-minimal.ava.js new file mode 100644 index 00000000..474dd5d0 --- /dev/null +++ b/benchmark/__tests__/test-highlevel-minimal.ava.js @@ -0,0 +1,51 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; +import { logGasDetail } from "./util.js"; + +test.before(async (t) => { + // Init the worker and start a Sandbox server + const worker = await Worker.init(); + + // Prepare sandbox for tests, create accounts, deploy contracts, etx. + const root = worker.rootAccount; + + // Deploy the test contract. + const highlevelContract = await root.devDeploy( + "build/highlevel-minimal.wasm" + ); + const highlevelContractRs = await root.devDeploy( + "res/highlevel_minimal.wasm" + ); + + // Test users + const ali = await root.createSubAccount("ali"); + const bob = await root.createSubAccount("bob"); + const carl = await root.createSubAccount("carl"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { + root, + highlevelContract, + highlevelContractRs, + ali, + bob, + carl, + }; +}); + +test("JS highlevel minimal contract", async (t) => { + const { bob, highlevelContract } = t.context.accounts; + let r = await bob.callRaw(highlevelContract, "empty", ""); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); + +test("RS highlevel minimal contract", async (t) => { + const { bob, highlevelContractRs } = t.context.accounts; + let r = await bob.callRaw(highlevelContractRs, "empty", ""); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); diff --git a/benchmark/__tests__/test-lowlevel-api.ava.js b/benchmark/__tests__/test-lowlevel-api.ava.js new file mode 100644 index 00000000..aeffa40e --- /dev/null +++ b/benchmark/__tests__/test-lowlevel-api.ava.js @@ -0,0 +1,59 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; +import { logGasDetail } from "./util.js"; + +test.before(async (t) => { + // Init the worker and start a Sandbox server + const worker = await Worker.init(); + + // Prepare sandbox for tests, create accounts, deploy contracts, etx. + const root = worker.rootAccount; + + // Deploy the test contract. + const lowlevelContract = await root.devDeploy("build/lowlevel-api.wasm"); + const lowlevelContractRs = await root.devDeploy("res/lowlevel_api.wasm"); + + // Test users + const ali = await root.createSubAccount("ali"); + const bob = await root.createSubAccount("bob"); + const carl = await root.createSubAccount("carl"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { + root, + lowlevelContract, + lowlevelContractRs, + ali, + bob, + carl, + }; +}); + +test("JS lowlevel API contract", async (t) => { + const { bob, lowlevelContract } = t.context.accounts; + let r = await bob.callRaw(lowlevelContract, "lowlevel_storage_write", ""); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); + +test("RS lowlevel API contract", async (t) => { + const { bob, lowlevelContractRs } = t.context.accounts; + let r = await bob.callRaw(lowlevelContractRs, "lowlevel_storage_write", ""); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); + +test("JS lowlevel API contract, call many", async (t) => { + const { bob, lowlevelContract } = t.context.accounts; + let r = await bob.callRaw( + lowlevelContract, + "lowlevel_storage_write_many", + "" + ); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); diff --git a/benchmark/__tests__/test-lowlevel-minimal.ava.js b/benchmark/__tests__/test-lowlevel-minimal.ava.js new file mode 100644 index 00000000..cc169745 --- /dev/null +++ b/benchmark/__tests__/test-lowlevel-minimal.ava.js @@ -0,0 +1,47 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; +import { logGasDetail } from "./util.js"; + +test.before(async (t) => { + // Init the worker and start a Sandbox server + const worker = await Worker.init(); + + // Prepare sandbox for tests, create accounts, deploy contracts, etx. + const root = worker.rootAccount; + + // Deploy the test contract. + const lowlevelContract = await root.devDeploy("build/lowlevel-minimal.wasm"); + const lowlevelContractRs = await root.devDeploy("res/lowlevel_minimal.wasm"); + + // Test users + const ali = await root.createSubAccount("ali"); + const bob = await root.createSubAccount("bob"); + const carl = await root.createSubAccount("carl"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { + root, + lowlevelContract, + lowlevelContractRs, + ali, + bob, + carl, + }; +}); + +test("JS lowlevel minimal contract", async (t) => { + const { bob, lowlevelContract } = t.context.accounts; + let r = await bob.callRaw(lowlevelContract, "empty", ""); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); + +test("RS lowlevel minimal contract", async (t) => { + const { bob, lowlevelContractRs } = t.context.accounts; + let r = await bob.callRaw(lowlevelContractRs, "empty", ""); + + t.is(r.result.status.SuccessValue, ""); + logGasDetail(r, t); +}); diff --git a/benchmark/__tests__/util.js b/benchmark/__tests__/util.js new file mode 100644 index 00000000..c43f502c --- /dev/null +++ b/benchmark/__tests__/util.js @@ -0,0 +1,49 @@ +export function formatGas(gas) { + if (gas < 10 ** 12) { + let tGas = gas / 10 ** 12; + let roundTGas = Math.round(tGas * 100000) / 100000; + return roundTGas + "T"; + } + let tGas = gas / 10 ** 12; + let roundTGas = Math.round(tGas * 100) / 100; + return roundTGas + "T"; +} + +export function gasBreakdown(outcome) { + return new Map( + outcome.metadata.gas_profile.map((g) => { + return [g.cost, Number(g.gas_used)]; + }) + ); +} + +export function logGasBreakdown(map, t) { + map.forEach((v, k) => { + t.log(" ", k, ": ", formatGas(v)); + }); +} + +export function logGasDetail(r, t) { + t.log( + "Gas used to convert transaction to receipt: ", + formatGas(r.result.transaction_outcome.outcome.gas_burnt) + ); + t.log( + "Gas used to execute the receipt (actual contract call): ", + formatGas(r.result.receipts_outcome[0].outcome.gas_burnt) + ); + let map = gasBreakdown(r.result.receipts_outcome[0].outcome); + logGasBreakdown(map, t); + t.log( + "Gas used to refund unused gas: ", + formatGas(r.result.receipts_outcome[1].outcome.gas_burnt) + ); + t.log( + "Total gas used: ", + formatGas( + r.result.transaction_outcome.outcome.gas_burnt + + r.result.receipts_outcome[0].outcome.gas_burnt + + r.result.receipts_outcome[1].outcome.gas_burnt + ) + ); +} diff --git a/benchmark/ava.config.cjs b/benchmark/ava.config.cjs new file mode 100644 index 00000000..d5a8c029 --- /dev/null +++ b/benchmark/ava.config.cjs @@ -0,0 +1,8 @@ +require("util").inspect.defaultOptions.depth = 5; // Increase AVA's printing depth + +module.exports = { + timeout: "300000", + files: ["**/*.ava.js"], + failWithoutAssertions: false, + extensions: ["js"], +}; diff --git a/benchmark/example-outcome.json b/benchmark/example-outcome.json new file mode 100644 index 00000000..4245cf77 --- /dev/null +++ b/benchmark/example-outcome.json @@ -0,0 +1,112 @@ +{ + "result": { + "receipts_outcome": [ + { + "block_hash": "B5zQjLFz1zu36xoUd37Bghzeanw8YfbkLk9amcVdPX4H", + "id": "7GaMgKm53Mv5hwimyLTQnxEvaacvSABHhhJXbQYFkiXi", + "outcome": { + "executor_id": "dev-14843.test.near", + "gas_burnt": 4217783714059, + "logs": [], + "metadata": { + "gas_profile": [ + { + "cost": "CONTRACT_LOADING_BASE", + "cost_category": "WASM_HOST_COST", + "gas_used": "35445963" + }, + { + "cost": "CONTRACT_LOADING_BYTES", + "cost_category": "WASM_HOST_COST", + "gas_used": "108947436750" + }, + { + "cost": "WASM_INSTRUCTION", + "cost_category": "WASM_HOST_COST", + "gas_used": "1680864179808" + } + ], + "version": 1 + }, + "receipt_ids": ["8FYdTYgNnz5jnCDFFHfbyrBVq5rp9beStZYjxjrXvQ5F"], + "status": { + "SuccessValue": "" + }, + "tokens_burnt": "4217783714059000000000" + }, + "proof": [] + }, + { + "block_hash": "4oUZamxqDyUuMp47xfGE7FTnRBRC28VdMuPCS3oSCKpw", + "id": "8FYdTYgNnz5jnCDFFHfbyrBVq5rp9beStZYjxjrXvQ5F", + "outcome": { + "executor_id": "bob.test.near", + "gas_burnt": 223182562500, + "logs": [], + "metadata": { + "gas_profile": [], + "version": 1 + }, + "receipt_ids": [], + "status": { + "SuccessValue": "" + }, + "tokens_burnt": "0" + }, + "proof": [] + } + ], + "status": { + "SuccessValue": "" + }, + "transaction": { + "actions": [ + { + "FunctionCall": { + "args": "IiI=", + "deposit": "0", + "gas": 30000000000000, + "method_name": "empty" + } + } + ], + "hash": "9TvjxGooYJ7A1Ju6NHocss2uCqPeUKiEv1EwPoiXUsS2", + "nonce": 10000001, + "public_key": "ed25519:5iyD5kpiWwBgW3vunXJdmj66ArJAZBhGUxHEud6UKbyr", + "receiver_id": "dev-14843.test.near", + "signature": "ed25519:5jb67kd3VoTnBWxpSgyHENdWerGwLaa5QJHaKVvaxrZosymia2cKf3DNC4ES3PHZ7mXHkky5JTheqnzzjCpop7fx", + "signer_id": "bob.test.near" + }, + "transaction_outcome": { + "block_hash": "FQAFEjJVuBJK6Mf64DxfpMPWifP8jyEUJc6zW3yn1rca", + "id": "9TvjxGooYJ7A1Ju6NHocss2uCqPeUKiEv1EwPoiXUsS2", + "outcome": { + "executor_id": "bob.test.near", + "gas_burnt": 2427936651538, + "logs": [], + "metadata": { + "gas_profile": null, + "version": 1 + }, + "receipt_ids": ["7GaMgKm53Mv5hwimyLTQnxEvaacvSABHhhJXbQYFkiXi"], + "status": { + "SuccessReceiptId": "7GaMgKm53Mv5hwimyLTQnxEvaacvSABHhhJXbQYFkiXi" + }, + "tokens_burnt": "2427936651538000000000" + }, + "proof": [] + } + }, + "startMs": 1675654730297, + "endMs": 1675654732818, + "config": { + "network": "sandbox", + "rootAccountId": "test.near", + "rpcAddr": "http://localhost:6163", + "initialBalance": "100000000000000000000000000", + "homeDir": "/tmp/sandbox/7a047a19-1712-48ba-9704-d5831cf91f2a", + "port": 6163, + "rm": false, + "refDir": null + } +} diff --git a/benchmark/jsconfig.json b/benchmark/jsconfig.json new file mode 100644 index 00000000..a6d61863 --- /dev/null +++ b/benchmark/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + }, + "exclude": ["node_modules"] +} diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 00000000..7b706545 --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,34 @@ +{ + "name": "bench", + "version": "1.0.0", + "description": "near-sdk-js benchmark", + "main": "index.js", + "type": "module", + "scripts": { + "build": "run-s build:*", + "build:lowlevel-minimal": "near-sdk-js build src/lowlevel-minimal.js build/lowlevel-minimal.wasm", + "build:highlevel-minimal": "near-sdk-js build src/highlevel-minimal.js build/highlevel-minimal.wasm", + "build:lowlevel-api": "near-sdk-js build src/lowlevel-api.js build/lowlevel-api.wasm", + "build:highlevel-collection": "near-sdk-js build src/highlevel-collection.js build/highlevel-collection.wasm", + "build:expensive-calc": "near-sdk-js build src/expensive-calc.js build/expensive-calc.wasm", + "build:deploy-contract": "near-sdk-js build src/deploy-contract.js build/deploy-contract.wasm", + "test": "ava", + "test:lowlevel-minimal": "ava __tests__/test-lowlevel-minimal.ava.js", + "test:highlevel-minimal": "ava __tests__/test-highlevel-minimal.ava.js", + "test:lowlevel-api": "ava __tests__/test-lowlevel-api.ava.js", + "test:highlevel-collection": "ava __tests__/test-highlevel-collection.ava.js", + "test:expensive-calc": "ava __tests__/test-expensive-calc.ava.js", + "test:deploy-contract": "ava __tests__/test-deploy-contract.ava.js" + }, + "author": "Near Inc ", + "license": "Apache-2.0", + "devDependencies": { + "ava": "^4.2.0", + "near-workspaces": "3.2.1", + "npm-run-all": "^4.1.5" + }, + "dependencies": { + "typescript": "^4.7.4", + "near-sdk-js": "workspace:*" + } +} diff --git a/benchmark/res/deploy_contract.wasm b/benchmark/res/deploy_contract.wasm new file mode 100755 index 00000000..4d54a9b4 Binary files /dev/null and b/benchmark/res/deploy_contract.wasm differ diff --git a/benchmark/res/expensive_calc.wasm b/benchmark/res/expensive_calc.wasm new file mode 100755 index 00000000..d89892ec Binary files /dev/null and b/benchmark/res/expensive_calc.wasm differ diff --git a/benchmark/res/highlevel_collection.wasm b/benchmark/res/highlevel_collection.wasm new file mode 100755 index 00000000..beae1805 Binary files /dev/null and b/benchmark/res/highlevel_collection.wasm differ diff --git a/benchmark/res/highlevel_minimal.wasm b/benchmark/res/highlevel_minimal.wasm new file mode 100755 index 00000000..25e2f4b2 Binary files /dev/null and b/benchmark/res/highlevel_minimal.wasm differ diff --git a/benchmark/res/lowlevel_api.wasm b/benchmark/res/lowlevel_api.wasm new file mode 100755 index 00000000..16318cd8 Binary files /dev/null and b/benchmark/res/lowlevel_api.wasm differ diff --git a/benchmark/res/lowlevel_minimal.wasm b/benchmark/res/lowlevel_minimal.wasm new file mode 100755 index 00000000..e519ff1c Binary files /dev/null and b/benchmark/res/lowlevel_minimal.wasm differ diff --git a/benchmark/src/deploy-contract.js b/benchmark/src/deploy-contract.js new file mode 100644 index 00000000..b5a82fa3 --- /dev/null +++ b/benchmark/src/deploy-contract.js @@ -0,0 +1,19 @@ +import { near } from "near-sdk-js"; + +export function deploy_contract() { + let promiseId = near.promiseBatchCreate("a.caller.test.near"); + near.promiseBatchActionCreateAccount(promiseId); + near.promiseBatchActionTransfer(promiseId, 10000000000000000000000000n); + near.promiseBatchActionDeployContract( + promiseId, + includeBytes("../../tests/build/promise_api.wasm") + ); + near.promiseBatchActionFunctionCall( + promiseId, + "cross_contract_callee", + "abc", + 0, + 2 * Math.pow(10, 13) + ); + near.promiseReturn(promiseId); +} diff --git a/benchmark/src/expensive-calc.js b/benchmark/src/expensive-calc.js new file mode 100644 index 00000000..c5a18731 --- /dev/null +++ b/benchmark/src/expensive-calc.js @@ -0,0 +1,15 @@ +import { NearBindgen, call, near } from "near-sdk-js"; + +@NearBindgen({}) +export class ExpensiveCalc { + @call({}) + expensive({ n }) { + let ret = 0; + let sign = 1; + for (let i = 0; i < n; i++) { + ret += i * sign; + sign *= -1; + } + near.valueReturn(ret.toString()); + } +} diff --git a/benchmark/src/highlevel-collection.js b/benchmark/src/highlevel-collection.js new file mode 100644 index 00000000..435951e3 --- /dev/null +++ b/benchmark/src/highlevel-collection.js @@ -0,0 +1,13 @@ +import { NearBindgen, call, UnorderedMap } from "near-sdk-js"; + +@NearBindgen({}) +export class HighlevelCollection { + constructor() { + this.unorderedMap = new UnorderedMap("a"); + } + + @call({}) + set({ key, value }) { + this.unorderedMap.set(key, value); + } +} diff --git a/benchmark/src/highlevel-minimal.js b/benchmark/src/highlevel-minimal.js new file mode 100644 index 00000000..f540e7ee --- /dev/null +++ b/benchmark/src/highlevel-minimal.js @@ -0,0 +1,7 @@ +import { NearBindgen, call } from "near-sdk-js"; + +@NearBindgen({}) +export class HighlevelMinimal { + @call({}) + empty({}) {} +} diff --git a/benchmark/src/lowlevel-api.js b/benchmark/src/lowlevel-api.js new file mode 100644 index 00000000..b2e1ec55 --- /dev/null +++ b/benchmark/src/lowlevel-api.js @@ -0,0 +1,20 @@ +import { near } from "near-sdk-js"; + +export function lowlevel_storage_write() { + let data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + near.storageWriteRaw(data, data); +} + +export function lowlevel_storage_write_many() { + let data = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); + near.storageWriteRaw(data, data); +} diff --git a/benchmark/src/lowlevel-minimal.js b/benchmark/src/lowlevel-minimal.js new file mode 100644 index 00000000..1611536f --- /dev/null +++ b/benchmark/src/lowlevel-minimal.js @@ -0,0 +1,3 @@ +import { near } from "near-sdk-js"; + +export function empty() {} diff --git a/benchmark/tsconfig.json b/benchmark/tsconfig.json new file mode 100644 index 00000000..1b051266 --- /dev/null +++ b/benchmark/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "experimentalDecorators": true, + "target": "es2020", + "noEmit": true + }, + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a359e84f..ff8c62c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,21 @@ importers: devDependencies: turbo: 1.6.3 + benchmark: + specifiers: + ava: ^4.2.0 + near-sdk-js: workspace:* + near-workspaces: 3.2.1 + npm-run-all: ^4.1.5 + typescript: ^4.7.4 + dependencies: + near-sdk-js: link:../packages/near-sdk-js + typescript: 4.9.3 + devDependencies: + ava: 4.3.3 + near-workspaces: 3.2.1 + npm-run-all: 4.1.5 + examples: specifiers: '@types/lodash-es': ^4.17.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c7f51148..27b8e651 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - "examples" - "tests" - "packages/*" + - "benchmark" \ No newline at end of file diff --git a/tests/src/promise_batch_api.js b/tests/src/promise_batch_api.js index fe61d6fd..2e78d7de 100644 --- a/tests/src/promise_batch_api.js +++ b/tests/src/promise_batch_api.js @@ -77,7 +77,6 @@ export function test_promise_batch_deploy_call() { near.promiseBatchActionCreateAccount(promiseId); near.promiseBatchActionTransfer(promiseId, 10000000000000000000000000n); // deploy content of promise_api.wasm to `b.caller2.test.near` - // Note, we do not use `bytes()`, it's too expensive for long bytes and exceed gas limit near.promiseBatchActionDeployContract( promiseId, includeBytes("../build/promise_api.wasm")