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
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
node-version-file: ".nvmrc"
cache: "npm"
cache-dependency-path: "./package-lock.json"
- name: Install dependencies
run: npm i

- name: Test
run: npm run test
run: npm run test
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
261 changes: 175 additions & 86 deletions benchmark.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const Benchmark = require('benchmark');
const { faker } = require('@faker-js/faker');
const chalk = require('chalk');
const fastJsonFormat = require('./src/index.js');
const JSONbig = require('json-bigint');
const LosslessJSON = require('lossless-json');
const jsoncParser = require('jsonc-parser');
const Benchmark = require("benchmark");
const { faker } = require("@faker-js/faker");
const chalk = require("chalk");
const fastJsonFormat = require("./src/index.js");
const JSONbig = require("json-bigint");
const LosslessJSON = require("lossless-json");
const jsoncParser = require("jsonc-parser");

/**
* Generates a nested JSON string of approximately the target size
Expand All @@ -23,170 +23,259 @@ function generateNestedJSON(targetSizeBytes) {
city: faker.location.city(),
state: faker.location.state(),
country: faker.location.country(),
zipCode: faker.location.zipCode()
zipCode: faker.location.zipCode(),
},
company: {
name: faker.company.name(),
catchPhrase: faker.company.catchPhrase(),
bs: faker.company.buzzPhrase()
bs: faker.company.buzzPhrase(),
},
phone: faker.phone.number(),
website: faker.internet.url(),
createdAt: faker.date.past().toISOString()
createdAt: faker.date.past().toISOString(),
});

// Cache some users for performance
const CACHED_USERS_COUNT = 100;
const cachedUsers = Array.from({ length: CACHED_USERS_COUNT }, generateUser);

// Calculate average size of one user
const sampleUserJson = JSON.stringify(cachedUsers[0]);
const AVERAGE_USER_SIZE = Buffer.byteLength(sampleUserJson);

// Estimate number of users needed
const estimatedUsersNeeded = Math.ceil(targetSizeBytes / AVERAGE_USER_SIZE);

const users = [];
let currentSize = 12; // Size of '{"users":[]}'
let userIndex = 0;

// Build up users array until we reach target size
while (currentSize < targetSizeBytes && userIndex < estimatedUsersNeeded * 1.2) {
while (
currentSize < targetSizeBytes &&
userIndex < estimatedUsersNeeded * 1.2
) {
// Use cached user but vary the id for uniqueness
const user = {
...cachedUsers[userIndex % CACHED_USERS_COUNT],
id: faker.string.uuid()
id: faker.string.uuid(),
};

users.push(user);

// Estimate current size (with JSON overhead)
const userJson = JSON.stringify(user);
currentSize += Buffer.byteLength(userJson) + (userIndex > 0 ? 1 : 0); // Add 1 for comma
userIndex++;
}

return JSON.stringify({ users });
}

const testSizes = [
{ name: '100 KB', bytes: 100 * 1024 },
{ name: '1 MB', bytes: 1024 * 1024 },
{ name: '5 MB', bytes: 5 * 1024 * 1024 },
{ name: '10 MB', bytes: 10 * 1024 * 1024 }
{ name: "100 KB", bytes: 100 * 1024 },
{ name: "1 MB", bytes: 1024 * 1024 },
{ name: "5 MB", bytes: 5 * 1024 * 1024 },
{ name: "10 MB", bytes: 10 * 1024 * 1024 },
];

console.log('\n' + chalk.bold.cyan('πŸš€ Fast JSON Format Benchmark') + '\n');
console.log(chalk.gray('⚑ Comparing ') + chalk.yellow('fast-json-format') + chalk.gray(' vs ') + chalk.yellow('jsonc-parser') + chalk.gray(' vs ') + chalk.yellow('json-bigint') + chalk.gray(' vs ') + chalk.yellow('lossless-json') + chalk.gray(' vs ') + chalk.yellow('JSON.stringify(JSON.parse())') + '\n');
console.log(chalk.bold.blue('πŸ“Š Generating test data...') + '\n');
console.log("\n" + chalk.bold.cyan("πŸš€ Fast JSON Format Benchmark") + "\n");
console.log(
chalk.gray("⚑ Comparing ") +
chalk.yellow("fast-json-format") +
chalk.gray(" vs ") +
chalk.yellow("jsonc-parser") +
chalk.gray(" vs ") +
chalk.yellow("json-bigint") +
chalk.gray(" vs ") +
chalk.yellow("lossless-json") +
chalk.gray(" vs ") +
chalk.yellow("JSON.stringify(JSON.parse())") +
"\n",
);
console.log(chalk.bold.blue("πŸ“Š Generating test data...") + "\n");

const testCases = testSizes.map(size => {
console.log(chalk.gray(' ⏳ Generating ') + chalk.cyan(size.name) + chalk.gray(' JSON...'));
const testCases = testSizes.map((size) => {
console.log(
chalk.gray(" ⏳ Generating ") +
chalk.cyan(size.name) +
chalk.gray(" JSON..."),
);
const data = generateNestedJSON(size.bytes);
const actualSize = data.length;
console.log(chalk.gray(' βœ… Generated: ') + chalk.green(`${(actualSize / 1024).toFixed(1)} KB`) + chalk.gray(` (${((actualSize / size.bytes) * 100).toFixed(1)}% of target)`));
console.log(
chalk.gray(" βœ… Generated: ") +
chalk.green(`${(actualSize / 1024).toFixed(1)} KB`) +
chalk.gray(
` (${((actualSize / size.bytes) * 100).toFixed(1)}% of target)`,
),
);
return {
name: size.name,
data: data,
actualSize: actualSize
actualSize: actualSize,
};
});

console.log('\n' + chalk.bold.magenta('🏁 Running benchmarks...') + '\n');
console.log("\n" + chalk.bold.magenta("🏁 Running benchmarks...") + "\n");

// Store all results for summary table
const allResults = [];

testCases.forEach(testCase => {
console.log('\n' + chalk.bold.yellow(`⚑ ${testCase.name}`) + ' ' + chalk.gray('━'.repeat(50)));
console.log(chalk.gray(' Size: ') + chalk.cyan(`${(testCase.actualSize / 1024).toFixed(1)} KB`));

testCases.forEach((testCase) => {
console.log(
"\n" +
chalk.bold.yellow(`⚑ ${testCase.name}`) +
" " +
chalk.gray("━".repeat(50)),
);
console.log(
chalk.gray(" Size: ") +
chalk.cyan(`${(testCase.actualSize / 1024).toFixed(1)} KB`),
);

const suite = new Benchmark.Suite();

const results = [];

suite
.add('fast-json-format', function() {
fastJsonFormat(testCase.data, ' ');
.add("fast-json-format", function () {
fastJsonFormat(testCase.data, " ");
})
.add('jsonc-parser', function() {
.add("jsonc-parser", function () {
JSON.stringify(jsoncParser.parse(testCase.data), null, 2);
})
.add('json-bigint', function() {
.add("json-bigint", function () {
JSONbig.stringify(JSONbig.parse(testCase.data), null, 2);
})
.add('lossless-json', function() {
.add("lossless-json", function () {
LosslessJSON.stringify(LosslessJSON.parse(testCase.data), null, 2);
})
.add('JSON.stringify', function() {
.add("JSON.stringify", function () {
JSON.stringify(JSON.parse(testCase.data), null, 2);
})
.on('cycle', function(event) {
.on("cycle", function (event) {
const name = event.target.name;
const ops = event.target.hz.toLocaleString('en-US', { maximumFractionDigits: 0 });
const ops = event.target.hz.toLocaleString("en-US", {
maximumFractionDigits: 0,
});
const margin = event.target.stats.rme.toFixed(2);

results.push({ name, hz: event.target.hz });

const symbol = results.length === 1 ? 'β”œβ”€' : results.length === 2 ? 'β”œβ”€' : results.length === 3 ? 'β”œβ”€' : results.length === 4 ? 'β”œβ”€' : '└─';
const color = name === 'fast-json-format' ? chalk.green : name === 'JSON.stringify' ? chalk.blue : name === 'json-bigint' ? chalk.magenta : name === 'jsonc-parser' ? chalk.cyan : chalk.yellow;

console.log(chalk.gray(` ${symbol} `) + color(name) + chalk.gray(': ') + chalk.bold.white(ops) + chalk.gray(' ops/sec Β±' + margin + '%'));

const symbol =
results.length === 1
? "β”œβ”€"
: results.length === 2
? "β”œβ”€"
: results.length === 3
? "β”œβ”€"
: results.length === 4
? "β”œβ”€"
: "└─";
const color =
name === "fast-json-format"
? chalk.green
: name === "JSON.stringify"
? chalk.blue
: name === "json-bigint"
? chalk.magenta
: name === "jsonc-parser"
? chalk.cyan
: chalk.yellow;

console.log(
chalk.gray(` ${symbol} `) +
color(name) +
chalk.gray(": ") +
chalk.bold.white(ops) +
chalk.gray(" ops/sec Β±" + margin + "%"),
);
})
.on('complete', function() {
const fastest = this.filter('fastest')[0];
const slowest = this.filter('slowest')[0];
.on("complete", function () {
const fastest = this.filter("fastest")[0];
const slowest = this.filter("slowest")[0];
const speedup = fastest.hz / slowest.hz;

// Store results for summary
allResults.push({
size: testCase.name,
results: results
results: results,
});
})
.run({ 'async': false });
.run({ async: false });
});

// Display summary table
console.log('\n\n' + chalk.bold.cyan('πŸ“Š Summary Table') + '\n');
console.log("\n\n" + chalk.bold.cyan("πŸ“Š Summary Table") + "\n");

// Build table header
const libs = ['fast-json-format', 'jsonc-parser', 'json-bigint', 'lossless-json', 'JSON.stringify'];
const libs = [
"fast-json-format",
"jsonc-parser",
"json-bigint",
"lossless-json",
"JSON.stringify",
];
const colWidths = { size: 12, lib: 20 };

// Header
console.log(
chalk.bold.white('Size'.padEnd(colWidths.size)) + ' β”‚ ' +
chalk.bold.green('fast-json-format'.padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.bold.cyan('jsonc-parser'.padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.bold.magenta('json-bigint'.padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.bold.yellow('lossless-json'.padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.bold.blue('JSON.stringify'.padEnd(colWidths.lib))
chalk.bold.white("Size".padEnd(colWidths.size)) +
" β”‚ " +
chalk.bold.green("fast-json-format".padEnd(colWidths.lib)) +
" β”‚ " +
chalk.bold.cyan("jsonc-parser".padEnd(colWidths.lib)) +
" β”‚ " +
chalk.bold.magenta("json-bigint".padEnd(colWidths.lib)) +
" β”‚ " +
chalk.bold.yellow("lossless-json".padEnd(colWidths.lib)) +
" β”‚ " +
chalk.bold.blue("JSON.stringify".padEnd(colWidths.lib)),
);
console.log(
"─".repeat(colWidths.size) +
"─┼─" +
"─".repeat(colWidths.lib) +
"─┼─" +
"─".repeat(colWidths.lib) +
"─┼─" +
"─".repeat(colWidths.lib) +
"─┼─" +
"─".repeat(colWidths.lib) +
"─┼─" +
"─".repeat(colWidths.lib),
);
console.log('─'.repeat(colWidths.size) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib) + '─┼─' + '─'.repeat(colWidths.lib));

// Rows
allResults.forEach(result => {
const fastJson = result.results.find(r => r.name === 'fast-json-format');
const jsonBigint = result.results.find(r => r.name === 'json-bigint');
const losslessJson = result.results.find(r => r.name === 'lossless-json');
const jsoncParser = result.results.find(r => r.name === 'jsonc-parser');
const jsonStringify = result.results.find(r => r.name === 'JSON.stringify');
const fastJsonOps = fastJson ? fastJson.hz.toFixed(0) : 'N/A';
const jsonBigintOps = jsonBigint ? jsonBigint.hz.toFixed(0) : 'N/A';
const losslessJsonOps = losslessJson ? losslessJson.hz.toFixed(0) : 'N/A';
const jsoncParserOps = jsoncParser ? jsoncParser.hz.toFixed(0) : 'N/A';
const jsonStringifyOps = jsonStringify ? jsonStringify.hz.toFixed(0) : 'N/A';
allResults.forEach((result) => {
const fastJson = result.results.find((r) => r.name === "fast-json-format");
const jsonBigint = result.results.find((r) => r.name === "json-bigint");
const losslessJson = result.results.find((r) => r.name === "lossless-json");
const jsoncParser = result.results.find((r) => r.name === "jsonc-parser");
const jsonStringify = result.results.find((r) => r.name === "JSON.stringify");

const fastJsonOps = fastJson ? fastJson.hz.toFixed(0) : "N/A";
const jsonBigintOps = jsonBigint ? jsonBigint.hz.toFixed(0) : "N/A";
const losslessJsonOps = losslessJson ? losslessJson.hz.toFixed(0) : "N/A";
const jsoncParserOps = jsoncParser ? jsoncParser.hz.toFixed(0) : "N/A";
const jsonStringifyOps = jsonStringify ? jsonStringify.hz.toFixed(0) : "N/A";

console.log(
chalk.cyan(result.size.padEnd(colWidths.size)) + ' β”‚ ' +
chalk.white((fastJsonOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.white((jsoncParserOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.white((jsonBigintOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.white((losslessJsonOps + ' ops/sec').padEnd(colWidths.lib)) + ' β”‚ ' +
chalk.white((jsonStringifyOps + ' ops/sec').padEnd(colWidths.lib))
chalk.cyan(result.size.padEnd(colWidths.size)) +
" β”‚ " +
chalk.white((fastJsonOps + " ops/sec").padEnd(colWidths.lib)) +
" β”‚ " +
chalk.white((jsoncParserOps + " ops/sec").padEnd(colWidths.lib)) +
" β”‚ " +
chalk.white((jsonBigintOps + " ops/sec").padEnd(colWidths.lib)) +
" β”‚ " +
chalk.white((losslessJsonOps + " ops/sec").padEnd(colWidths.lib)) +
" β”‚ " +
chalk.white((jsonStringifyOps + " ops/sec").padEnd(colWidths.lib)),
);
});

console.log('\n' + chalk.gray('Note: Higher ops/sec = better performance') + '\n');
console.log(
"\n" + chalk.gray("Note: Higher ops/sec = better performance") + "\n",
);
2 changes: 1 addition & 1 deletion license.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
Loading