Skip to content

Commit

Permalink
Performance (#15)
Browse files Browse the repository at this point in the history
* Performance improvements

* Heavily optimize string read/write functions

* Update dependencies

* Change to 1 minute runs for each benchmark

* Add RPC benchmarks

* Remove benchmark
  • Loading branch information
pouya-eghbali authored Nov 26, 2024
1 parent 13770d1 commit 3dcf22b
Show file tree
Hide file tree
Showing 20 changed files with 1,903 additions and 200 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ isolate-*
*.ignore
coverage
dist
.yarn
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
29 changes: 0 additions & 29 deletions package-lock.json

This file was deleted.

24 changes: 21 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,30 @@
"scripts": {
"build": "tsc",
"lint": "tslint -p tsconfig.json",
"prepublishOnly": "npm run build"
"prepublishOnly": "yarn build",
"benchmark": "yarn build && yarn node dist/benchmark/index.js",
"benchmark:ws:server": "yarn build && yarn node dist/benchmark/ws/simple/server.js",
"benchmark:ws": "yarn build && yarn node dist/benchmark/ws/simple/index.js",
"benchmark:ws:server:heavy": "yarn build && yarn node dist/benchmark/ws/heavy/server.js",
"benchmark:ws:heavy": "yarn build && yarn node dist/benchmark/ws/heavy/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.4.3"
}
"@faker-js/faker": "^9.2.0",
"@types/node": "^22.9.0",
"@types/ws": "^8",
"cbor-x": "^1.6.0",
"msgpackr": "^1.11.2",
"sializer": "0",
"tinybench": "^3.0.6",
"typescript": "^5.4.3",
"ws": "^8.18.0"
},
"dependencies": {
"utfz-lib": "^0.2.0"
},
"packageManager": "yarn@4.4.1",
"type": "module"
}
32 changes: 32 additions & 0 deletions src/ascii.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const asciiToUint8Array = (
str: string,
strLength: number,
buffer: Uint8Array,
offset: number
) => {
for (let i = 0; i < strLength; i++) {
buffer[offset + i] = str.charCodeAt(i);
}
return strLength;
};

const fns = new Array(66).fill(0).map((_, i) => {
const codes = new Array(i)
.fill(0)
.map((_, j) => `buf[offset + ${j}]`)
.join(", ");
return new Function(
"buf",
"length",
"offset",
`return String.fromCharCode(${codes});`
);
});

export const uint8ArrayToAscii = (
buffer: Uint8Array,
byteLength: number,
offset: number
) => {
return fns[byteLength](buffer, byteLength, offset);
};
56 changes: 56 additions & 0 deletions src/benchmark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Bench } from "tinybench";
import {
siaFiveThousandUsers,
siaFiveThousandUsersDecode,
} from "./tests/sia.js";
import {
jsonFiveThousandUsers,
jsonFiveThousandUsersDecode,
} from "./tests/json.js";
import {
cborFiveThousandUsers,
cborFiveThousandUsersDecode,
} from "./tests/cbor.js";
import {
siaOneFiveThousandUsers,
siaOneFiveThousandUsersDecode,
} from "./tests/sia-v1.js";
import {
msgpackrFiveThousandUsers,
msgpackrFiveThousandUsersDecode,
} from "./tests/msgpackr.js";

const bench = new Bench({ name: "serialization", time: 60 * 1000 });

bench.add("JSON", () => jsonFiveThousandUsers());
bench.add("Sializer", () => siaFiveThousandUsers());
bench.add("Sializer (v1)", () => siaOneFiveThousandUsers());
bench.add("CBOR-X", () => cborFiveThousandUsers());
bench.add("MsgPackr", () => msgpackrFiveThousandUsers());

console.log(`Running ${bench.name} benchmark...`);
await bench.run();

console.table(bench.table());

const deserializeBench = new Bench({
name: "deserialization",
time: 60 * 1000,
});

deserializeBench.add("JSON", () => jsonFiveThousandUsersDecode());
deserializeBench.add("Sializer", () => siaFiveThousandUsersDecode());
deserializeBench.add("Sializer (v1)", () => siaOneFiveThousandUsersDecode());
deserializeBench.add("CBOR-X", () => cborFiveThousandUsersDecode());
deserializeBench.add("MsgPackr", () => msgpackrFiveThousandUsersDecode());

console.log(`Running ${deserializeBench.name} benchmark...`);
await deserializeBench.run();

console.table(deserializeBench.table());

console.log("Sia file size:", siaFiveThousandUsers().length);
console.log("Sia v1 file size:", siaOneFiveThousandUsers().length);
console.log("JSON file size:", jsonFiveThousandUsers().length);
console.log("MsgPackr file size:", cborFiveThousandUsers().length);
console.log("CBOR-X file size:", msgpackrFiveThousandUsers().length);
8 changes: 8 additions & 0 deletions src/benchmark/tests/cbor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { fiveThousandUsers } from "./common.js";
import { encode, decode } from "cbor-x";

export const cborFiveThousandUsers = () => encode(fiveThousandUsers);

const encoded = cborFiveThousandUsers();

export const cborFiveThousandUsersDecode = () => decode(encoded);
25 changes: 25 additions & 0 deletions src/benchmark/tests/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { faker } from "@faker-js/faker";

export function createRandomUser() {
return {
userId: faker.string.uuid(),
username: faker.internet.username(),
email: faker.internet.email(),
avatar: faker.image.avatar(),
password: faker.internet.password(),
birthdate: faker.date.birthdate(),
registeredAt: faker.date.past(),
};
}

export const fiveUsers = faker.helpers.multiple(createRandomUser, {
count: 5,
});

export const fiveHundredUsers = faker.helpers.multiple(createRandomUser, {
count: 500,
});

export const fiveThousandUsers = faker.helpers.multiple(createRandomUser, {
count: 5_000,
});
8 changes: 8 additions & 0 deletions src/benchmark/tests/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { fiveThousandUsers } from "./common.js";

export const jsonFiveThousandUsers = () =>
Buffer.from(JSON.stringify(fiveThousandUsers));

const encoded = jsonFiveThousandUsers();

export const jsonFiveThousandUsersDecode = () => JSON.parse(encoded.toString());
8 changes: 8 additions & 0 deletions src/benchmark/tests/msgpackr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { fiveThousandUsers } from "./common.js";
import { pack, unpack } from "msgpackr";

export const msgpackrFiveThousandUsers = () => pack(fiveThousandUsers);

const encoded = msgpackrFiveThousandUsers();

export const msgpackrFiveThousandUsersDecode = () => unpack(encoded);
8 changes: 8 additions & 0 deletions src/benchmark/tests/sia-v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { fiveThousandUsers } from "./common.js";
import { sia, desia } from "sializer";

export const siaOneFiveThousandUsers = () => sia(fiveThousandUsers);

const encoded = siaOneFiveThousandUsers();

export const siaOneFiveThousandUsersDecode = () => desia(encoded);
36 changes: 36 additions & 0 deletions src/benchmark/tests/sia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { fiveThousandUsers } from "./common.js";
import { Sia } from "../../index.js";
import assert from "assert";

const sia = new Sia();

export const siaFiveThousandUsers = () =>
sia
.seek(0)
.addArray16(fiveThousandUsers, (sia, user) => {
sia
.addAscii(user.userId)
.addAscii(user.username)
.addAscii(user.email)
.addAscii(user.avatar)
.addAscii(user.password)
.addInt64(user.birthdate.valueOf())
.addInt64(user.registeredAt.valueOf());
})
.toUint8ArrayReference();

const encoded = siaFiveThousandUsers();
const desia = new Sia(encoded);

const decodeUser = (sia: Sia) => ({
userId: sia.readAscii(),
username: sia.readAscii(),
email: sia.readAscii(),
avatar: sia.readAscii(),
password: sia.readAscii(),
birthdate: new Date(sia.readInt64()),
registeredAt: new Date(sia.readInt64()),
});

export const siaFiveThousandUsersDecode = () =>
desia.seek(0).readArray16(decodeUser);
108 changes: 108 additions & 0 deletions src/benchmark/ws/heavy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Bench } from "tinybench";
import WebSocket from "ws";
import { Sia } from "../../../index.js";
import { pack, unpack } from "msgpackr";
import { decode, encode } from "cbor-x";
import { fiveHundredUsers } from "../../tests/common.js";

const sia = new Sia();

const rpcRequest = {
method: "batchCalculateUserAges",
params: fiveHundredUsers,
};

export const payloads = {
sia: () =>
sia
.seek(0)
.addAscii(rpcRequest.method)
.addArray16(rpcRequest.params, (sia, user) => {
sia
.addAscii(user.userId)
.addAscii(user.username)
.addAscii(user.email)
.addAscii(user.avatar)
.addAscii(user.password)
.addInt64(user.birthdate.getTime())
.addInt64(user.registeredAt.getTime());
})
.toUint8ArrayReference(),
json: () => new Uint8Array(Buffer.from(JSON.stringify(rpcRequest))),
cbor: () => new Uint8Array(encode(rpcRequest)),
msgpack: () => new Uint8Array(pack(rpcRequest)),
};

const clients = {
sia: new WebSocket("ws://localhost:8080"),
cbor: new WebSocket("ws://localhost:8081"),
msgpack: new WebSocket("ws://localhost:8082"),
json: new WebSocket("ws://localhost:8083"),
};

const callbacks = {
sia: (data: Buffer) => {
return new Sia(new Uint8Array(data)).readArray16((sia) => {
const userId = sia.readAscii();
const age = sia.readUInt8();
return { userId, age };
});
},
cbor: (data: Buffer) => decode(data),
msgpack: (data: Buffer) => unpack(data),
json: (data: Buffer) => JSON.parse(data.toString()),
};

console.log("Waiting for connections...");
await new Promise((resolve) => setTimeout(resolve, 15 * 1000));

const bench = new Bench({ name: "RPC", time: 10 * 1000 });

const makeRpcCall = async (
ws: WebSocket,
ondata: (data: Buffer) => void,
payload: Uint8Array
) =>
new Promise((resolve) => {
ws.send(payload, { binary: true });
const done = (data: Buffer) => {
ws.off("message", done);
ondata(data);
resolve(null);
};
ws.on("message", done);
});

bench
.add(
"JSON",
async () => await makeRpcCall(clients.json, callbacks.json, payloads.json())
)
.addEventListener("complete", () => clients.json.close());

bench
.add(
"Sia",
async () => await makeRpcCall(clients.sia, callbacks.sia, payloads.sia())
)
.addEventListener("complete", () => clients.sia.close());

bench
.add(
"CBOR",
async () => await makeRpcCall(clients.cbor, callbacks.cbor, payloads.cbor())
)
.addEventListener("complete", () => clients.cbor.close());

bench
.add(
"MsgPack",
async () =>
await makeRpcCall(clients.msgpack, callbacks.msgpack, payloads.msgpack())
)
.addEventListener("complete", () => clients.msgpack.close());

console.log(`Running ${bench.name} benchmark...`);
await bench.run();

console.table(bench.table());
Loading

0 comments on commit 3dcf22b

Please sign in to comment.