Skip to content
Draft
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
15,482 changes: 15,482 additions & 0 deletions benchmark/fixtures/twitter.json
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Large diffs are not rendered by default.

192 changes: 192 additions & 0 deletions benchmark/http/json-parsing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
'use strict';

const common = require('../common.js');
const http = require('http');

const configs = {
small: {
json: JSON.stringify({
id: 1,
name: 'Alice',
active: true,
score: 9.5,
}),
schema: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
active: { type: 'boolean' },
score: { type: 'number' },
},
},
},
medium: {
json: JSON.stringify({
order_id: 'ORD-12345',
created_at: '2024-01-01T00:00:00Z',
status: 'pending',
total: 109.97,
currency: 'USD',
user: { id: 1, name: 'Alice', email: 'alice@example.com' },
items: [
{ product_id: 'P1', name: 'Widget', quantity: 2, price: 29.99, sku: 'WGT-001' },
{ product_id: 'P2', name: 'Gadget', quantity: 1, price: 49.99, sku: 'GDG-001' },
],
shipping_address: {
street: '123 Main St',
city: 'Anytown',
state: 'CA',
zip: '12345',
country: 'US',
},
}),
schema: {
type: 'object',
properties: {
order_id: { type: 'string' },
status: { type: 'string' },
total: { type: 'number' },
items: {
type: 'array',
items: {
type: 'object',
properties: {
product_id: { type: 'string' },
quantity: { type: 'integer' },
price: { type: 'number' },
},
},
},
},
},
},
large: {
json: JSON.stringify({
id: 'usr_abc123',
username: 'alice_wonder',
email: 'alice@example.com',
full_name: 'Alice Wonderland',
created_at: '2020-01-01T00:00:00Z',
updated_at: '2024-06-15T12:30:00Z',
last_login: '2024-06-20T08:00:00Z',
is_active: true,
is_verified: true,
role: 'admin',
plan: 'enterprise',
preferences: {
theme: 'dark',
language: 'en',
notifications: { email: true, sms: false, push: true },
timezone: 'America/New_York',
date_format: 'MM/DD/YYYY',
},
profile: {
bio: 'Software Engineer',
website: 'https://example.com',
avatar_url: 'https://cdn.example.com/avatars/alice.jpg',
location: 'New York, NY',
company: 'Acme Corp',
job_title: 'Senior Engineer',
phone: '+1-555-0100',
},
stats: {
total_orders: 42,
total_spent: 1234.56,
loyalty_points: 9876,
reviews_count: 15,
avg_rating: 4.8,
},
tags: ['vip', 'early-adopter', 'beta-tester'],
addresses: [
{
id: 'addr_1',
type: 'billing',
street: '123 Main St',
city: 'New York',
state: 'NY',
zip: '10001',
country: 'US',
is_default: true,
},
{
id: 'addr_2',
type: 'shipping',
street: '456 Oak Ave',
city: 'Brooklyn',
state: 'NY',
zip: '11201',
country: 'US',
is_default: false,
},
],
payment_methods: [
{ id: 'pm_1', type: 'credit_card', last4: '4242', brand: 'Visa', exp_month: 12, exp_year: 2027 },
{ id: 'pm_2', type: 'paypal', email: 'alice@paypal.com' },
],
}),
schema: {
type: 'object',
properties: {
id: { type: 'string' },
username: { type: 'string' },
email: { type: 'string' },
is_active: { type: 'boolean' },
role: { type: 'string' },
},
},
},
};

const bench = common.createBenchmark(main, {
method: ['node', 'v8', 'ajv'],
payload: Object.keys(configs),
connections: [100, 500],
duration: 5,
});

function main({ method, payload, connections, duration }) {
const { json, schema } = configs[payload];

let parser;
let validate;
if (method === 'node') {
const { Parser } = require('node:json');
parser = new Parser(schema);
parser.parse(json); // Warm up before accepting connections.
} else if (method === 'ajv') {
const Ajv = require('ajv');
const ajv = new Ajv();
validate = ajv.compile(schema);
validate(JSON.parse(json)); // Warm up before accepting connections.
}

const response = Buffer.from('{"ok":true}');

const server = http.createServer((_req, res) => {
if (method === 'node') {
parser.parse(json);
} else if (method === 'ajv') {
validate(JSON.parse(json));
} else {
JSON.parse(json);
}

res.writeHead(200, {
'Content-Type': 'application/json',
'Content-Length': response.length,
});
res.end(response);
});

server.listen(0, () => {
bench.http({
path: '/',
connections,
duration,
port: server.address().port,
}, () => {
server.close();
});
});
}
59 changes: 59 additions & 0 deletions benchmark/misc/json-parser-twitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

// Benchmark using the twitter.json fixture (~631 KB, 100 tweets).
// Uses a lower default n than json-parser.js due to payload size.

const common = require('../common.js');
const assert = require('assert');
const { readFileSync } = require('fs');
const { join } = require('path');
const { Parser } = require('node:json');

const json = readFileSync(join(__dirname, '../fixtures/twitter.json'), 'utf8');

// Project 6 fields out of 23 per tweet; skip nested entities, user, etc.
const schema = {
type: 'object',
properties: {
statuses: {
type: 'array',
items: {
type: 'object',
properties: {
id_str: { type: 'string' },
text: { type: 'string' },
created_at: { type: 'string' },
retweet_count: { type: 'integer' },
favorite_count: { type: 'integer' },
lang: { type: 'string' },
},
},
},
},
};

const bench = common.createBenchmark(main, {
method: ['node', 'v8'],
n: [10],
});

function main({ method, n }) {
if (method === 'node') {
const parser = new Parser(schema);
let result = parser.parse(json); // Warm up / avoid dead code elimination
bench.start();
for (let i = 0; i < n; ++i) {
result = parser.parse(json);
}
bench.end(n);
assert.ok(result);
} else {
let result = JSON.parse(json);
bench.start();
for (let i = 0; i < n; ++i) {
result = JSON.parse(json);
}
bench.end(n);
assert.ok(result);
}
}
102 changes: 102 additions & 0 deletions benchmark/misc/json-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';

const common = require('../common.js');
const assert = require('assert');
const { Parser } = require('node:json');

const payloads = {
flat_small: {
json: '{"name":"Alice","age":30,"active":true,"score":9.5}',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
active: { type: 'boolean' },
score: { type: 'number' },
},
},
},
flat_large: {
json: JSON.stringify({
id: 1, name: 'Bob', email: 'bob@example.com', age: 25,
address: '123 Main St', city: 'Anytown', state: 'NY', zip: '10001',
country: 'US', phone: '555-1234', fax: '555-5678', website: 'http://example.com',
company: 'Acme', department: 'Engineering', title: 'Engineer',
manager: 'Carol', team: 'Backend', score: 8.2, active: true, tags: 'foo,bar',
}),
schema: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
score: { type: 'number' },
active: { type: 'boolean' },
},
},
},
nested: {
json: '{"user":{"id":42,"name":"Carol","roles":["admin","user"]},"meta":{"version":2,"debug":false}}',
schema: {
type: 'object',
properties: {
user: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
},
},
},
},
},
array: {
json: JSON.stringify(
Array.from({ length: 10 }, (_, i) => ({
id: i + 1, name: `Item ${i + 1}`, value: i * 1.5, active: i % 2 === 0,
description: `Description for item ${i + 1}`, extra: 'ignored',
})),
),
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
value: { type: 'number' },
active: { type: 'boolean' },
},
},
},
},
};

const bench = common.createBenchmark(main, {
method: ['node', 'v8'],
payload: Object.keys(payloads),
n: [1e4],
});

function main({ method, payload, n }) {
const { json, schema } = payloads[payload];

if (method === 'node') {
const parser = new Parser(schema);
let result = parser.parse(json); // Warm up / avoid dead code elimination
bench.start();
for (let i = 0; i < n; ++i) {
result = parser.parse(json);
}
bench.end(n);
assert.ok(result);
} else {
let result = JSON.parse(json);
bench.start();
for (let i = 0; i < n; ++i) {
result = JSON.parse(json);
}
bench.end(n);
assert.ok(result);
}
}
1 change: 1 addition & 0 deletions lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const legacyWrapperList = new SafeSet([
// beginning with "internal/".
// Modules that can only be imported via the node: scheme.
const schemelessBlockList = new SafeSet([
'json',
'sea',
'sqlite',
'quic',
Expand Down
5 changes: 5 additions & 0 deletions lib/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { Parser } = internalBinding('json_parser');

module.exports = { Parser };
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
'src/node_file.cc',
'src/node_file_utils.cc',
'src/node_http_parser.cc',
'src/node_json_parser.cc',
'src/node_http2.cc',
'src/node_i18n.cc',
'src/node_locks.cc',
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
V(internal_only_v8) \
V(js_stream) \
V(js_udp_wrap) \
V(json_parser) \
V(locks) \
V(messaging) \
V(modules) \
Expand Down
Loading