Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brown-carrots-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"devalue": minor
---

feat: use native alternatives to encode/decode base64
5 changes: 5 additions & 0 deletions .changeset/happy-mugs-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"devalue": minor
---

feat: simplify TypedArray slices
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
/node_modules
/types
/types
.results
121 changes: 121 additions & 0 deletions benchmarking/benchmarks/typed-array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { fastest_test } from '../utils.js';

import { parse, stringify } from '../../index.js';

const value_small = new Uint8Array(Array.from({ length: 100 }, (_, i) => i));
const value_medium = new Uint8Array(Array.from({ length: 10 * 1024 }, (_, i) => i % 256));
const value_large = new Uint8Array(Array.from({ length: 1024 * 1024 }, (_, i) => i % 256));

const string_small = stringify(value_small);
const string_medium = stringify(value_medium);
const string_large = stringify(value_large);

export default [
{
label: `stringify: small`,
async fn() {
const value = value_small;

// warm up
for (let i = 0; i < 10_000; i++) {
stringify(value);
}

return await fastest_test(3, () => {
for (let i = 0; i < 500_000; i++) {
stringify(value);
}
});
},
},

{
label: `stringify: medium`,
async fn() {
const value = value_medium;

// warm up
for (let i = 0; i < 1_000; i++) {
stringify(value);
}

return await fastest_test(3, () => {
for (let i = 0; i < 5_000; i++) {
stringify(value);
}
});
},
},

{
label: `stringify: large`,
async fn() {
const value = value_large;

// warm up
for (let i = 0; i < 10; i++) {
stringify(value);
}

return await fastest_test(3, () => {
for (let i = 0; i < 50; i++) {
stringify(value);
}
});
},
},

{
label: `parse: small`,
async fn() {
const string = string_small;

// warm up
for (let i = 0; i < 10_000; i++) {
parse(string);
}

return await fastest_test(3, () => {
for (let i = 0; i < 500_000; i++) {
parse(string);
}
});
},
},

{
label: `parse: medium`,
async fn() {
const string = string_medium;

// warm up
for (let i = 0; i < 1_000; i++) {
parse(string);
}

return await fastest_test(3, () => {
for (let i = 0; i < 5_000; i++) {
parse(string);
}
});
},
},

{
label: `parse: large`,
async fn() {
const string = string_large;

// warm up
for (let i = 0; i < 10; i++) {
parse(string);
}

return await fastest_test(3, () => {
for (let i = 0; i < 50; i++) {
parse(string);
}
});
},
},
];
111 changes: 111 additions & 0 deletions benchmarking/compare/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import fs from 'node:fs';
import path from 'node:path';
import { execSync, fork } from 'node:child_process';

/** @type {(command: string) => string} */
const exec = (command) => execSync(command).toString().trim();

const is_jj = execSync('git for-each-ref --count=1 refs/jj/').length > 0;

const current_ref = exec(
is_jj
? 'jj show --no-patch --template change_id'
: 'git symbolic-ref --short -q HEAD || git rev-parse --short HEAD',
);

/** @type {(branch: string) => void} */
const checkout = is_jj
? (branch) => exec(`jj edit ${branch}`)
: (branch) => exec(`git checkout ${branch}`);

const runner = path.resolve(import.meta.filename, '../runner.js');
const outdir = path.resolve(import.meta.filename, '../.results');

fs.rmSync(outdir, { recursive: true, force: true });
fs.mkdirSync(outdir);

/** @type {string[]} */
const branches = [];

for (const arg of process.argv.slice(2)) {
if (arg.startsWith('--')) continue;
if (arg === import.meta.filename) continue;

branches.push(arg);
}

if (branches.length === 0) {
branches.push(current_ref);
}

if (branches.length === 1) {
branches.push('main');
}

process.on('exit', () => checkout(current_ref));

for (const branch of branches) {
console.group(`Benchmarking ${branch}`);

checkout(branch);

await new Promise((fulfil, reject) => {
const child = fork(runner);

child.on('message', (results) => {
fs.writeFileSync(`${outdir}/${branch}.json`, JSON.stringify(results, null, ' '));
fulfil(undefined);
});

child.on('error', reject);
});

console.groupEnd();
}

const results = branches.map((branch) => {
return JSON.parse(fs.readFileSync(`${outdir}/${branch}.json`, 'utf-8'));
});

for (let i = 0; i < results[0].length; i += 1) {
console.group(`${results[0][i].benchmark}`);

for (const metric of ['time', 'gc_time']) {
const times = results.map((result) => +result[i][metric]);
let min = Infinity;
let max = -Infinity;
let min_index = -1;

for (let b = 0; b < times.length; b += 1) {
const time = times[b];

if (time < min) {
min = time;
min_index = b;
}

if (time > max) {
max = time;
}
}

if (min !== 0) {
console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
times.forEach((time, b) => {
const SIZE = 20;
const n = Math.round(SIZE * (time / max));

console.log(
`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`,
);
});
console.groupEnd();
}
}

console.groupEnd();
}

function char(i) {
return String.fromCharCode(97 + i);
}
13 changes: 13 additions & 0 deletions benchmarking/compare/runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import typedarray from '../benchmarks/typed-array.js';

const results = [];

for (let i = 0; i < typedarray.length; i += 1) {
const benchmark = typedarray[i];

process.stderr.write(`Running ${i + 1}/${typedarray.length} ${benchmark.label} `);
results.push({ benchmark: benchmark.label, ...(await benchmark.fn()) });
process.stderr.write('\x1b[2K\r');
}

process.send?.(results);
78 changes: 78 additions & 0 deletions benchmarking/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import typed_array_benchmarks from './benchmarks/typed-array.js';

// e.g. `pnpm bench typedarray` to only run the typedarray benchmarks
const filters = process.argv.slice(2);

/** @type {(b: { label: string }) => boolean} */
const filter_fn = filters.length ? (b) => filters.some((f) => b.label.includes(f)) : (b) => true;

const suites = [
{
name: 'TypedArray benchmarks',
benchmarks: typed_array_benchmarks.filter(filter_fn),
},
].filter((suite) => suite.benchmarks.length > 0);

if (suites.length === 0) {
console.log('No benchmarks matched provided filters');
process.exit(1);
}

const COLUMN_WIDTHS = [40, 9, 9];
const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b);

/** @type {(str: string, n: number) => string} */
const pad_right = (str, n) => str + ' '.repeat(n - str.length);

/** @type {(str: string, n: number) => string} */
const pad_left = (str, n) => ' '.repeat(n - str.length) + str;

let total_time = 0;
let total_gc_time = 0;

try {
for (const { benchmarks, name } of suites) {
let suite_time = 0;
let suite_gc_time = 0;

console.log(`\nRunning ${name}...\n`);
console.log(
pad_right('Benchmark', COLUMN_WIDTHS[0]) +
pad_left('Time', COLUMN_WIDTHS[1]) +
pad_left('GC time', COLUMN_WIDTHS[2]),
);
console.log('='.repeat(TOTAL_WIDTH));

for (const benchmark of benchmarks) {
const results = await benchmark.fn();
console.log(
pad_right(benchmark.label, COLUMN_WIDTHS[0]) +
pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) +
pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2]),
);
total_time += results.time;
total_gc_time += results.gc_time;
suite_time += results.time;
suite_gc_time += results.gc_time;
}

console.log('='.repeat(TOTAL_WIDTH));
console.log(
pad_right('suite', COLUMN_WIDTHS[0]) +
pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) +
pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2]),
);
console.log('='.repeat(TOTAL_WIDTH));
}
} catch (e) {
console.error(e);
process.exit(1);
}

console.log('');

console.log(
pad_right('total', COLUMN_WIDTHS[0]) +
pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) +
pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2]),
);
19 changes: 19 additions & 0 deletions benchmarking/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"noEmit": true,
"moduleResolution": "Bundler",
"target": "ESNext",
"module": "ESNext",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"checkJs": true,
"types": ["node"]
},
"include": ["./**/*.js"]
}
Loading
Loading