Skip to content

Instantly share code, notes, and snippets.

@guest271314
Last active January 20, 2025 12:39
Show Gist options
  • Save guest271314/df73f63b574f8fc0b869f8f39c48f1ed to your computer and use it in GitHub Desktop.
Save guest271314/df73f63b574f8fc0b869f8f39c48f1ed to your computer and use it in GitHub Desktop.
TypeScript .ts file execution benchmarks for Deno, Bun, Node.js
Deno.bench("Deno, Bun, Node.js TypeScript Benchmarks", async (b) => {
// Open a file that we will act upon.
// using file = await Deno.open("a_big_data_file.txt");
// Tell the benchmarking tool that this is the only section you want
// to measure.
b.start();
// Now let's measure how long it takes to read all of the data from the file.
// await new Response(file.readable).arrayBuffer();
const [path, allowed_origin] = ["./nm_typescript.ts", "x"];
const command = new Deno.Command(path, {
args: [allowed_origin],
stdout: "piped",
stdin: "piped",
});
const subprocess = command.spawn();
const buffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 });
const view = new DataView(buffer);
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function encodeMessage(message) {
return encoder.encode(JSON.stringify(message));
}
async function* getMessage(readable) {
let messageLength = 0;
let readOffset = 0;
for await (let message of readable) {
if (buffer.byteLength === 0 && messageLength === 0) {
buffer.resize(4);
for (let i = 0; i < 4; i++) {
view.setUint8(i, message[i]);
}
messageLength = view.getUint32(0, true);
console.log({ messageLength });
message = message.subarray(4);
buffer.resize(0);
}
if (message.length) {
buffer.resize(buffer.byteLength + message.length);
for (let i = 0; i < message.length; i++, readOffset++) {
view.setUint8(readOffset, message[i]);
}
if (buffer.byteLength === messageLength) {
yield new Uint8Array(buffer);
messageLength = 0;
readOffset = 0;
buffer.resize(0);
}
}
}
}
(async () => {
try {
for await (const message of getMessage(subprocess.stdout)) {
console.log(JSON.parse(decoder.decode(message)));
}
} catch (e) {
console.log(e.message);
Deno.exit();
}
})();
let data = encodeMessage(Array(209715));
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage("test");
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage("");
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage(1);
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage(new Uint8Array([97]));
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
subprocess.kill("SIGTERM");
// End measurement here.
b.end();
});
{
"version": 1,
"runtime": "Deno/2.1.6+b962b87 x86_64-unknown-linux-gnu",
"cpu": "AMD A8-7410 APU with AMD Radeon R5 Graphics",
"benches": [
{
"origin": "file:///home/user/bench.js",
"group": null,
"name": "Deno, Bun, Node.js TypeScript Benchmarks",
"baseline": false,
"results": [
{
"ok": {
"n": 12,
"min": 361437526.0,
"max": 393814717.0,
"avg": 385186260.0,
"p75": 390455018.0,
"p99": 393814717.0,
"p995": 393814717.0,
"p999": 393814717.0,
"highPrecision": true,
"usedExplicitTimers": true
}
}
]
}
]
}
{
"version": 1,
"runtime": "Deno/2.1.6+b962b87 x86_64-unknown-linux-gnu",
"cpu": "AMD A8-7410 APU with AMD Radeon R5 Graphics",
"benches": [
{
"origin": "file:///home/user/bench.js",
"group": null,
"name": "Deno, Bun, Node.js TypeScript Benchmarks",
"baseline": false,
"results": [
{
"ok": {
"n": 12,
"min": 361970514.0,
"max": 387731958.0,
"avg": 370500352.0,
"p75": 372767552.0,
"p99": 387731958.0,
"p995": 387731958.0,
"p999": 387731958.0,
"highPrecision": true,
"usedExplicitTimers": true
}
}
]
}
]
}
// How to test the different hosts #2
// https://github.com/guest271314/NativeMessagingHosts/discussions/2
// deno -A nm_standalone_test.js ./nm_nodejs.js native-messaging-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/
const [path, allowed_origin] = Deno.args;
const command = new Deno.Command(path, {
args: [allowed_origin],
stdout: "piped",
stdin: "piped",
});
const subprocess = command.spawn();
const buffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 });
const view = new DataView(buffer);
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function encodeMessage(message) {#!/usr/bin/env -S /home/xubuntu/bin/node --no-warnings
return encoder.encode(JSON.stringify(message));
}
async function* getMessage(readable) {
let messageLength = 0;
let readOffset = 0;
for await (let message of readable) {
if (buffer.byteLength === 0 && messageLength === 0) {
buffer.resize(4);
for (let i = 0; i < 4; i++) {
view.setUint8(i, message[i]);
}
messageLength = view.getUint32(0, true);
console.log({ messageLength });
message = message.subarray(4);
buffer.resize(0);
}
if (message.length) {
buffer.resize(buffer.byteLength + message.length);
for (let i = 0; i < message.length; i++, readOffset++) {
view.setUint8(readOffset, message[i]);
}
if (buffer.byteLength === messageLength) {
yield new Uint8Array(buffer);
messageLength = 0;
readOffset = 0;
buffer.resize(0);
}
}
}
}
(async () => {
try {
for await (const message of getMessage(subprocess.stdout)) {
console.log(JSON.parse(decoder.decode(message)));
}
} catch (e) {
console.log(e.message);
Deno.exit();
}
})();
let data = encodeMessage(Array(209715));
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage("test");
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage("");
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage(1);
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true });
await new Promise((resolve) => setTimeout(resolve, 20));
data = encodeMessage(new Uint8Array([97]));
await new Blob([
new Uint8Array(new Uint32Array([data.length]).buffer),
data,
])
.stream()
.pipeTo(subprocess.stdin, { preventClose: true});
await new Promise((resolve) => setTimeout(resolve, 20));
subprocess.kill("SIGTERM");
/*
#!/usr/bin/env -S /home/user/deno -A
#!/usr/bin/env -S /home/user/bun
#!/usr/bin/env -S /home/user/node --no-warnings
TypeScript Native Messaging host
guest271314, 7-28-2024
*/
/*
declare let readable: NodeJS.ReadStream & {
fd: 0;
} | ReadableStream<Uint8Array>, writable: WritableStream<Uint8Array>, exit: () => void;
declare function encodeMessage(message: any): Uint8Array;
declare function getMessage(): AsyncGenerator<Uint8Array>;
declare function sendMessage(message: Uint8Array): Promise<void>;
export { encodeMessage, exit, getMessage, readable, sendMessage, writable, };
*/
// Source JavaScript: https://github.com/guest271314/NativeMessagingHosts/blob/main/nm_host.js
//
// Convert JavaScript to TypeScript, no obvious equivalent with tsc
// https://www.codeconvert.ai/javascript-to-typescript-converter
//
// Resizable ArrayBuffer supported by tsc Version 5.7.0-dev.20241019
/**
* /// <reference types="https://raw.githubusercontent.com/microsoft/TypeScript/2ac4cb78d6930302eb0a55d07f154a2b0597ae32/src/lib/es2024.arraybuffer.d.ts" />
*/
import process from "node:process";
const runtime: string = navigator.userAgent;
const buffer: ArrayBuffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 });
const view: DataView = new DataView(buffer);
const encoder: TextEncoder = new TextEncoder();
let readable: NodeJS.ReadStream & { fd: 0 } | ReadableStream<Uint8Array>,
writable: WritableStream<Uint8Array>,
exit: () => void = () => {};
if (runtime.startsWith("Deno")) {
// @ts-ignore Deno
({ readable } = Deno.stdin);
// @ts-ignore Deno
({ writable } = Deno.stdout);
// @ts-ignore Deno
({ exit } = Deno);
}
if (runtime.startsWith("Node")) {
readable = process.stdin;
writable = new WritableStream({
write(value) {
process.stdout.write(value);
},
}, new CountQueuingStrategy({ highWaterMark: Infinity }));
({ exit } = process);
}
if (runtime.startsWith("Bun")) {
// @ts-ignore Bun
readable = Bun.file("/dev/stdin").stream();
writable = new WritableStream<Uint8Array>({
async write(value) {
// @ts-ignore Bun
await Bun.write(Bun.stdout, value);
},
}, new CountQueuingStrategy({ highWaterMark: Infinity }));
({ exit } = process);
}
function encodeMessage(message: object): Uint8Array {
return encoder.encode(JSON.stringify(message));
}
async function* getMessage(): AsyncGenerator<Uint8Array> {
let messageLength: number = 0;
let readOffset: number = 0;
for await (let message of readable) {
if (buffer.byteLength === 0 && messageLength === 0) {
buffer.resize(4);
for (let i = 0; i < 4; i++) {
view.setUint8(i, message[i]);
}
messageLength = view.getUint32(0, true);
message = message.subarray(4);
buffer.resize(0);
}
buffer.resize(buffer.byteLength + message.length);
for (let i = 0; i < message.length; i++, readOffset++) {
view.setUint8(readOffset, message[i]);
}
if (buffer.byteLength === messageLength) {
yield new Uint8Array(buffer);
messageLength = 0;
readOffset = 0;
buffer.resize(0);
}
}
}
async function sendMessage(message: Uint8Array): Promise<void> {
await new Blob([
new Uint8Array(new Uint32Array([message.length]).buffer),
message,
])
.stream()
.pipeTo(writable, { preventClose: true });
}
try {
for await (const message of getMessage()) {
await sendMessage(message);
}
} catch (e: any) {
sendMessage(encodeMessage(e.message));
exit();
}
// export { encodeMessage, exit, getMessage, readable, sendMessage, writable };
{
"version": 1,
"runtime": "Deno/2.1.6+b962b87 x86_64-unknown-linux-gnu",
"cpu": "AMD A8-7410 APU with AMD Radeon R5 Graphics",
"benches": [
{
"origin": "file:///home/xubuntu/bin/bench.js",
"group": null,
"name": "Deno, Bun, Node.js TypeScript Benchmarks",
"baseline": false,
"results": [
{
"ok": {
"n": 12,
"min": 479875419.0,
"max": 517102463.0,
"avg": 502455798.0,
"p75": 513776413.0,
"p99": 517102463.0,
"p995": 517102463.0,
"p999": 517102463.0,
"highPrecision": true,
"usedExplicitTimers": true
}
}
]
}
]
}

TypeScript .ts file execution benchmarks for Deno, Bun, Node.js

We'll be benchmarking execution of Microsoft TypeScript .ts file execution in canary/nightly Deno, Bun, Node.js.

deno and bun have been capable of executing .ts files directly for a while. node recently added that capability; initially behind a flags --experimental-strip-types or --experimental-transform-types, now without a flag being necessary; see Modules: TypeScript | Node.js v23.6.0 Documentation and 2025-01-07, Version 23.6.0 (Current) #56450.

The file being tested is a standalone Native Messaging test. That is, testing the Native Messaging protocol outside of the browser.

For completeness here is Native Messaging description as appears in Chrome Developers, MDN Web Docs, Microsoft Edge Developer documentation, and Safari web extension documentation

This is the protocol

Native messaging protocol (Chrome Developers)

Chrome starts each native messaging host in a separate process and communicates with it using standard input (stdin) and standard output (stdout). The same format is used to send messages in both directions; each message is serialized using JSON, UTF-8 encoded and is preceded with 32-bit message length in native byte order. The maximum size of a single message from the native messaging host is 1 MB, mainly to protect Chrome from misbehaving native applications. The maximum size of the message sent to the native messaging host is 4 GB.

The test when run standalone, without benchmarking, should print this output, a representation of the communication between the browser and the native application, shell script, or program that implements the host

deno -A nm_standalone_test.js ./nm_typescript.ts native-messaging-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{ messageLength: 1048576 }
[
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  null, null, null, null, null, null, null, null, null, null,
  ... 209615 more items
]
{ messageLength: 6 }
test
{ messageLength: 2 }

{ messageLength: 1 }
1
{ messageLength: 8 }
{ "0": 97 }

The file being tested is below, nm_typescript.ts. We'll be manually adjusting the shebang line to the given JavaScript/TypeScript runtime between tests.

The file that we will be using to simulate the communication between the browser and the Native Messaging host is nm_standalone_test.js, where we start a subprocess to execute nm_typescript.ts, implementing the Native Messaging protocol. We have embedded the text of nm_typescript.ts in the benchmarking code, which uses Deno.bench().

Notice in nm_typescript.ts JavaScript runtimes do not process stdin and stdout the same. ECMA-262 does not specify I/O for JavaScript. JavaScript engines and runtimes may, or may not, implement reading stdin and writing to stdout.

What I have done is use WHATWG Streams for reading stdin and writing to stdout, in an effort to use the same or mostly similar code for each runtime, for the ability to actually test the same code in each runtime, in a runtime agnostic manner.

We'll be testing using the lastest deno and bun canary releases, and node nightly fetched from the respective sources, today.

  • deno 2.1.6+b962b87 (canary, release, x86_64-unknown-linux-gnu)
  • bun 1.1.45
  • node v24.0.0-nightly20250119009d53ec3c

Adjusting the shebang for each runtime

  #!/usr/bin/env -S /home/user/deno -A
  #!/usr/bin/env -S /home/user/bun
  #!/usr/bin/env -S /home/user/node --no-warnings

Executed at the command line as

deno bench -A --json bench.js

Summarized results

deno

"min": 361970514.0,
"max": 387731958.0,
"avg": 370500352.0,

bun

"min": 361437526.0,
"max": 393814717.0,
"avg": 385186260.0,

node

"min": 479875419.0,
"max": 517102463.0,
"avg": 502455798.0,

Bun yields the fastest min, Deno yields the fastest avg, Node.js yields the slowest min, avg, and max for executing .ts file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment