From e1b4cf05ad4183b9d30e7cfb463c7b4b2f430110 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Thu, 5 Sep 2024 14:05:42 +0300 Subject: [PATCH 1/3] Add gas usage test --- .../test-collections-performance.ava.js | 145 ++++++++++++++++++ benchmark/__tests__/util.js | 15 ++ benchmark/package.json | 8 +- benchmark/src/lookup-map.js | 23 +++ benchmark/src/lookup-set.js | 23 +++ benchmark/src/unordered-map.js | 31 ++++ benchmark/src/unordered-set.js | 31 ++++ benchmark/src/vector.js | 31 ++++ 8 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 benchmark/__tests__/test-collections-performance.ava.js create mode 100644 benchmark/src/lookup-map.js create mode 100644 benchmark/src/lookup-set.js create mode 100644 benchmark/src/unordered-map.js create mode 100644 benchmark/src/unordered-set.js create mode 100644 benchmark/src/vector.js diff --git a/benchmark/__tests__/test-collections-performance.ava.js b/benchmark/__tests__/test-collections-performance.ava.js new file mode 100644 index 000000000..c7a862a82 --- /dev/null +++ b/benchmark/__tests__/test-collections-performance.ava.js @@ -0,0 +1,145 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; +import { logTotalGas, randomInt } from "./util.js"; + +const COLLECTION_SIZE = 20; + +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 contracts. + const lookupMapContract = await root.devDeploy("build/lookup-map.wasm"); + const lookupSetContract = await root.devDeploy("build/lookup-set.wasm"); + const unorderedMapContract = await root.devDeploy("build/unordered-map.wasm"); + const unorderedSetContract = await root.devDeploy("build/unordered-set.wasm"); + const vectorContract = await root.devDeploy("build/vector.wasm"); + + // Test users + const ali = await root.createSubAccount("ali"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { + root, + lookupMapContract, + lookupSetContract, + unorderedMapContract, + unorderedSetContract, + vectorContract, + ali, + }; +}); + +test("JS lookup map contract operations", async (t) => { + const { ali, lookupMapContract } = t.context.accounts; + + let rAdd; + for (let i = 0; i < COLLECTION_SIZE; i++) { + rAdd = await ali.callRaw(lookupMapContract, "addElement", { key: i, value: i }); + } + t.is(rAdd.result.status.SuccessValue, ""); + logTotalGas("Add element", rAdd, t); + + const val = randomInt(COLLECTION_SIZE); + const rGet = await ali.callRaw(lookupMapContract, "getElement", { key: val }); + t.is(JSON.parse(Buffer.from(rGet.result.status.SuccessValue, "base64")), val); + logTotalGas("Get element", rGet, t); + + const rRem = await ali.callRaw(lookupMapContract, "removeElement", { key: randomInt(COLLECTION_SIZE) }); + t.is(rRem.result.status.SuccessValue, ""); + logTotalGas("Remove element", rRem, t); +}); + +test("JS lookup set contract operations", async (t) => { + const { ali, lookupSetContract } = t.context.accounts; + + let rAdd; + for (let i = 0; i < COLLECTION_SIZE; i++) { + rAdd = await ali.callRaw(lookupSetContract, "addElement", { value: i }); + } + t.is(rAdd.result.status.SuccessValue, ""); + logTotalGas("Add element", rAdd, t); + + const rGet = await ali.callRaw(lookupSetContract, "containsElement", { value: randomInt(COLLECTION_SIZE) }); + t.is(JSON.parse(Buffer.from(rGet.result.status.SuccessValue, "base64")), true); + logTotalGas("Get element", rGet, t); + + const rRem = await ali.callRaw(lookupSetContract, "removeElement", { value: randomInt(COLLECTION_SIZE) }); + t.is(rRem.result.status.SuccessValue, ""); + logTotalGas("Remove element", rRem, t); +}); + +test("JS unordered map contract operations", async (t) => { + const { ali, unorderedMapContract } = t.context.accounts; + + let rAdd; + for (let i = 0; i < COLLECTION_SIZE; i++) { + rAdd = await ali.callRaw(unorderedMapContract, "addElement", { key: i, value: i }); + } + t.is(rAdd.result.status.SuccessValue, ""); + logTotalGas("Add element", rAdd, t); + + const val = randomInt(COLLECTION_SIZE); + const rGet = await ali.callRaw(unorderedMapContract, "getElement", { key: val }); + t.is(JSON.parse(Buffer.from(rGet.result.status.SuccessValue, "base64")), val); + logTotalGas("Get element", rGet, t); + + const rIt = await ali.callRaw(unorderedMapContract, "iterate", {}); + t.is(rIt.result.status.SuccessValue, ""); + logTotalGas("Iterate collection", rIt, t); + + const rRem = await ali.callRaw(unorderedMapContract, "removeElement", { key: randomInt(COLLECTION_SIZE) }); + t.is(rRem.result.status.SuccessValue, ""); + logTotalGas("Remove element", rRem, t); +}); + +test("JS unordered set contract operations", async (t) => { + const { ali, unorderedSetContract } = t.context.accounts; + + let rAdd; + for (let i = 0; i < COLLECTION_SIZE; i++) { + rAdd = await ali.callRaw(unorderedSetContract, "addElement", { value: i }); + } + t.is(rAdd.result.status.SuccessValue, ""); + logTotalGas("Add element", rAdd, t); + + const rGet = await ali.callRaw(unorderedSetContract, "containsElement", { value: randomInt(COLLECTION_SIZE) }); + t.is(JSON.parse(Buffer.from(rGet.result.status.SuccessValue, "base64")), true); + logTotalGas ("Get element", rGet, t); + + const rIt = await ali.callRaw(unorderedSetContract, "iterate", {}); + t.is(rIt.result.status.SuccessValue, ""); + logTotalGas("Iterate collection", rIt, t); + + const rRem = await ali.callRaw(unorderedSetContract, "removeElement", { value: randomInt(COLLECTION_SIZE) }); + t.is(rRem.result.status.SuccessValue, ""); + logTotalGas("Remove element", rRem, t); +}); + +test("JS vector contract operations", async (t) => { + const { ali, vectorContract } = t.context.accounts; + + let rAdd; + for (let i = 0; i < COLLECTION_SIZE; i++) { + rAdd = await ali.callRaw(vectorContract, "addElement", { value: i }); + } + t.is(rAdd.result.status.SuccessValue, ""); + logTotalGas("Add element", rAdd, t); + + const val = randomInt(COLLECTION_SIZE); + const rGet = await ali.callRaw(vectorContract, "getElement", { index: val }); + t.is(JSON.parse(Buffer.from(rGet.result.status.SuccessValue, "base64")), val); + logTotalGas("Get element", rGet, t); + + const rIt = await ali.callRaw(vectorContract, "iterate", {}); + t.is(rIt.result.status.SuccessValue, ""); + logTotalGas("Iterate collection", rIt, t); + + const rRem = await ali.callRaw(vectorContract, "removeElement", { index: randomInt(COLLECTION_SIZE) }); + t.is(rRem.result.status.SuccessValue, ""); + logTotalGas("Remove element", rRem, t); +}); \ No newline at end of file diff --git a/benchmark/__tests__/util.js b/benchmark/__tests__/util.js index c43f502c3..c6ad59164 100644 --- a/benchmark/__tests__/util.js +++ b/benchmark/__tests__/util.js @@ -47,3 +47,18 @@ export function logGasDetail(r, t) { ) ); } + +export function logTotalGas(prefix = '', r, t) { + t.log( + prefix + ' - 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 + ) + ); +} + +export function randomInt(max) { + return Math.floor(Math.random() * max); +} \ No newline at end of file diff --git a/benchmark/package.json b/benchmark/package.json index e5233fefb..f4dca8908 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -12,13 +12,19 @@ "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", + "build:lookup-map": "near-sdk-js build src/lookup-map.js build/lookup-map.wasm", + "build:lookup-set": "near-sdk-js build src/lookup-set.js build/lookup-set.wasm", + "build:unordered-map": "near-sdk-js build src/unordered-map.js build/unordered-map.wasm", + "build:unordered-set": "near-sdk-js build src/unordered-set.js build/unordered-set.wasm", + "build:vector": "near-sdk-js build src/vector.js build/vector.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" + "test:deploy-contract": "ava __tests__/test-deploy-contract.ava.js", + "test:collections": "ava __tests__/test-collections-performance.ava.js" }, "author": "Near Inc ", "license": "Apache-2.0", diff --git a/benchmark/src/lookup-map.js b/benchmark/src/lookup-map.js new file mode 100644 index 000000000..ccca0126d --- /dev/null +++ b/benchmark/src/lookup-map.js @@ -0,0 +1,23 @@ +import { NearBindgen, call, LookupMap, view } from "near-sdk-js"; + +@NearBindgen({}) +export class LookupMapContract { + constructor() { + this.lookupMap = new LookupMap("LM"); + } + + @call({}) + addElement({ key, value }) { + this.lookupMap.set(key, value); + } + + @call({}) + removeElement({ key }) { + this.lookupMap.remove(key); + } + + @view({}) + getElement({ key }) { + return this.lookupMap.get(key); + } +} diff --git a/benchmark/src/lookup-set.js b/benchmark/src/lookup-set.js new file mode 100644 index 000000000..7d23c2f0b --- /dev/null +++ b/benchmark/src/lookup-set.js @@ -0,0 +1,23 @@ +import { NearBindgen, call, LookupSet, view } from "near-sdk-js"; + +@NearBindgen({}) +export class LookupSetContract { + constructor() { + this.lookupSet = new LookupSet("LS"); + } + + @call({}) + addElement({ value }) { + this.lookupSet.set(value); + } + + @call({}) + removeElement({ value }) { + this.lookupSet.remove(value); + } + + @view({}) + containsElement({ value }) { + return this.lookupSet.contains(value); + } +} diff --git a/benchmark/src/unordered-map.js b/benchmark/src/unordered-map.js new file mode 100644 index 000000000..c35b05287 --- /dev/null +++ b/benchmark/src/unordered-map.js @@ -0,0 +1,31 @@ +import { NearBindgen, call, UnorderedMap, view } from "near-sdk-js"; + +@NearBindgen({}) +export class UnorderedMapContract { + constructor() { + this.unorderedMap = new UnorderedMap("UM"); + } + + @call({}) + addElement({ key, value }) { + this.unorderedMap.set(key, value); + } + + @call({}) + removeElement({ key }) { + this.unorderedMap.remove(key); + } + + @view({}) + getElement({ key }) { + return this.unorderedMap.get(key); + } + + @view({}) + iterate() { + const size = this.unorderedMap.length; + for (let i = 0; i < size; i++) { + this.unorderedMap.get(i); + } + } +} diff --git a/benchmark/src/unordered-set.js b/benchmark/src/unordered-set.js new file mode 100644 index 000000000..644f5adc3 --- /dev/null +++ b/benchmark/src/unordered-set.js @@ -0,0 +1,31 @@ +import { NearBindgen, call, UnorderedSet, view } from "near-sdk-js"; + +@NearBindgen({}) +export class UnorderedSetContract { + constructor() { + this.unorderedSet = new UnorderedSet("US"); + } + + @call({}) + addElement({ value }) { + this.unorderedSet.set(value); + } + + @call({}) + removeElement({ value }) { + this.unorderedSet.remove(value); + } + + @view({}) + containsElement({ value }) { + return this.unorderedSet.contains(value); + } + + @view({}) + iterate() { + const size = this.unorderedSet.length; + for (let i = 0; i < size; i++) { + this.unorderedSet.contains(i); + } + } +} diff --git a/benchmark/src/vector.js b/benchmark/src/vector.js new file mode 100644 index 000000000..88e87036a --- /dev/null +++ b/benchmark/src/vector.js @@ -0,0 +1,31 @@ +import { NearBindgen, call, Vector, view } from "near-sdk-js"; + +@NearBindgen({}) +export class VectorContract { + constructor() { + this.vector = new Vector("V"); + } + + @call({}) + addElement({ value }) { + this.vector.push(value); + } + + @call({}) + removeElement({ index }) { + this.vector.swapRemove(index); + } + + @view({}) + getElement({ index }) { + return this.vector.get(index); + } + + @view({}) + iterate() { + const size = this.vector.length; + for (let i = 0; i < size; i++) { + this.vector.get(i); + } + } +} From 69b2351675cd1444671110925f486a13103bb721 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Mon, 9 Sep 2024 11:43:10 +0300 Subject: [PATCH 2/3] Tear down worker --- benchmark/__tests__/test-collections-performance.ava.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/benchmark/__tests__/test-collections-performance.ava.js b/benchmark/__tests__/test-collections-performance.ava.js index c7a862a82..858599c0d 100644 --- a/benchmark/__tests__/test-collections-performance.ava.js +++ b/benchmark/__tests__/test-collections-performance.ava.js @@ -34,6 +34,12 @@ test.before(async (t) => { }; }); +test.after.always(async (t) => { + await t.context.worker.tearDown().catch((error) => { + console.log("Failed to tear down the worker:", error); + }); +}); + test("JS lookup map contract operations", async (t) => { const { ali, lookupMapContract } = t.context.accounts; From 03055c9f64323267cc989178663875fbb1afa8f3 Mon Sep 17 00:00:00 2001 From: Georgi Tsonev Date: Mon, 30 Sep 2024 11:05:46 +0300 Subject: [PATCH 3/3] fix logTotalGas method --- benchmark/__tests__/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/__tests__/util.js b/benchmark/__tests__/util.js index c41acb628..a768d4e54 100644 --- a/benchmark/__tests__/util.js +++ b/benchmark/__tests__/util.js @@ -56,7 +56,7 @@ export function logTotalGas(prefix = '', r, t) { 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[1]?.outcome.gas_burnt || 0) ) ); }