mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
Signed-off-by: StefanStojanovic <stefan.stojanovic@janeasystems.com> PR-URL: https://github.com/nodejs/node/pull/63696 Refs: https://github.com/nodejs/node/issues/61964 Reviewed-By: Richard Lau <richard.lau@ibm.com>
342 lines
12 KiB
JavaScript
342 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
// PGO Training Script: JSON Processing Workload
|
|
//
|
|
// JSON.parse() and JSON.stringify() are among the most frequently called
|
|
// functions in Node.js applications (every REST API request/response cycle).
|
|
// This script exercises the JSON parser and serializer with:
|
|
// - Small objects (API responses, config)
|
|
// - Medium objects (paginated lists, user profiles)
|
|
// - Large objects (analytics payloads, data exports)
|
|
// - Nested structures (deep objects, arrays of objects)
|
|
// - Various value types (strings, numbers, booleans, null, arrays, objects)
|
|
// - Edge cases (unicode, escaped characters, large numbers, empty objects)
|
|
// - Realistic patterns: parse → transform → stringify pipeline
|
|
//
|
|
// This exercises: V8 JSON parser, V8 JSON serializer, string allocation,
|
|
// property access patterns, array iteration, GC pressure from allocations.
|
|
|
|
const DURATION_MS = parseInt(process.env.PGO_TRAINING_DURATION, 10) || 15_000;
|
|
const ITERATIONS_PER_BATCH = 500;
|
|
|
|
// Realistic JSON payloads matching common API shapes
|
|
|
|
// 1. Small config/health check (~100 bytes)
|
|
const SMALL_PAYLOADS = [
|
|
'{"status":"ok","uptime":12345,"version":"1.0.0"}',
|
|
'{"id":42,"name":"test","active":true,"score":98.5}',
|
|
'{"error":null,"data":{"key":"value"},"meta":{}}',
|
|
'{"token":"abc123def456","expires":1700000000}',
|
|
'{"success":true,"count":0,"items":[]}',
|
|
];
|
|
|
|
// 2. Medium user/product objects (~1-5 KB)
|
|
function generateUserPayload(count) {
|
|
const users = [];
|
|
for (let i = 0; i < count; i++) {
|
|
users.push({
|
|
id: `usr_${i.toString(36).padStart(8, '0')}`,
|
|
email: `user${i}@company.example.com`,
|
|
name: { first: `First${i}`, last: `Last${i}` },
|
|
role: ['admin', 'editor', 'viewer', 'member'][i % 4],
|
|
permissions: ['read', 'write', 'delete', 'admin'].slice(0, (i % 4) + 1),
|
|
settings: {
|
|
theme: i % 2 === 0 ? 'dark' : 'light',
|
|
language: ['en', 'es', 'fr', 'de', 'ja'][i % 5],
|
|
notifications: { email: true, push: i % 3 !== 0, sms: false },
|
|
timezone: 'America/New_York',
|
|
},
|
|
metadata: {
|
|
createdAt: '2024-01-15T08:30:00.000Z',
|
|
updatedAt: '2024-12-01T14:22:33.456Z',
|
|
lastLogin: '2024-12-15T09:45:00.000Z',
|
|
loginCount: 100 + i * 7,
|
|
},
|
|
});
|
|
}
|
|
return JSON.stringify({ data: users, total: count, page: 1, perPage: count });
|
|
}
|
|
|
|
// 3. Large analytics/event payloads (~50-200 KB)
|
|
function generateEventPayload(count) {
|
|
const events = [];
|
|
for (let i = 0; i < count; i++) {
|
|
events.push({
|
|
id: `evt_${Date.now()}_${i}`,
|
|
type: [
|
|
'page_view',
|
|
'click',
|
|
'form_submit',
|
|
'api_call',
|
|
'error',
|
|
'purchase',
|
|
][i % 6],
|
|
timestamp: new Date(Date.now() - i * 60000).toISOString(),
|
|
session: `sess_${Math.floor(i / 10)}`,
|
|
user: i % 5 === 0 ? null : `usr_${i % 100}`,
|
|
properties: {
|
|
url: `https://example.com/page/${i % 20}?ref=dashboard&utm_source=email`,
|
|
title: `Page Title ${i % 20} — Application Dashboard`,
|
|
referrer: i % 3 === 0 ? 'https://google.com/search?q=test' : '',
|
|
duration: Math.round(Math.random() * 30000),
|
|
viewport: { width: 1920, height: 1080 },
|
|
device: {
|
|
type: ['desktop', 'mobile', 'tablet'][i % 3],
|
|
os: ['Windows 11', 'macOS 14', 'iOS 17', 'Android 14'][i % 4],
|
|
browser: ['Chrome 120', 'Firefox 121', 'Safari 17'][i % 3],
|
|
},
|
|
},
|
|
context: {
|
|
ip: `10.${i % 256}.${(i * 3) % 256}.${(i * 7) % 256}`,
|
|
locale: ['en-US', 'en-GB', 'es-ES', 'fr-FR', 'de-DE'][i % 5],
|
|
campaign:
|
|
i % 10 === 0 ?
|
|
{ name: 'holiday_sale', medium: 'email', source: 'mailchimp' } :
|
|
null,
|
|
},
|
|
});
|
|
}
|
|
return JSON.stringify({
|
|
events,
|
|
batch_id: `batch_${Date.now()}`,
|
|
sent_at: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
// 4. Deeply nested structure (config files, GraphQL responses)
|
|
function generateNestedPayload(depth, breadth) {
|
|
function nest(d) {
|
|
if (d === 0) return { value: 'leaf', count: 42, tags: ['a', 'b'] };
|
|
const obj = {};
|
|
for (let i = 0; i < breadth; i++) {
|
|
obj[`level_${d}_key_${i}`] = nest(d - 1);
|
|
}
|
|
return obj;
|
|
}
|
|
return JSON.stringify({ root: nest(depth), _meta: { depth, breadth } });
|
|
}
|
|
|
|
// 5. Array-heavy payload (table data, CSV-like)
|
|
function generateTablePayload(rows, cols) {
|
|
const headers = Array.from({ length: cols }, (_, i) => `column_${i}`);
|
|
const data = [];
|
|
for (let r = 0; r < rows; r++) {
|
|
const row = {};
|
|
for (let c = 0; c < cols; c++) {
|
|
row[headers[c]] =
|
|
c % 3 === 0 ? r * c : c % 3 === 1 ? `val_${r}_${c}` : r % 2 === 0;
|
|
}
|
|
data.push(row);
|
|
}
|
|
return JSON.stringify({ headers, data, rowCount: rows });
|
|
}
|
|
|
|
// 6. Unicode-heavy payload (i18n content)
|
|
const UNICODE_PAYLOAD = JSON.stringify({
|
|
messages: {
|
|
en: { greeting: 'Hello, World!', farewell: 'Goodbye!' },
|
|
ja: { greeting: 'こんにちは世界!', farewell: 'さようなら!' },
|
|
ko: { greeting: '안녕하세요 세계!', farewell: '안녕히 가세요!' },
|
|
zh: { greeting: '你好世界!', farewell: '再见!' },
|
|
ar: { greeting: 'مرحبا بالعالم!', farewell: 'مع السلامة!' },
|
|
ru: { greeting: 'Привет мир!', farewell: 'До свидания!' },
|
|
de: { greeting: 'Hallo Welt!', farewell: 'Auf Wiedersehen!' },
|
|
emoji: { greeting: '👋🌍✨', farewell: '👋😢💫' },
|
|
},
|
|
descriptions: Array.from(
|
|
{ length: 50 },
|
|
(_, i) =>
|
|
`Item ${i}: Ünîcödé tëst with spëcîal chars — «quotes» "double" 'single' & ampersand \\ backslash / slash`,
|
|
),
|
|
});
|
|
|
|
// Pre-generate payloads as strings
|
|
const mediumPayload10 = generateUserPayload(10);
|
|
const mediumPayload50 = generateUserPayload(50);
|
|
const largePayload100 = generateEventPayload(100);
|
|
const largePayload500 = generateEventPayload(500);
|
|
const nestedPayload = generateNestedPayload(5, 3);
|
|
const tablePayload = generateTablePayload(200, 15);
|
|
|
|
// All payloads weighted by real-world frequency
|
|
const PAYLOADS = [
|
|
// Small payloads (health checks, simple responses) - most frequent
|
|
...SMALL_PAYLOADS.map((p) => ({ json: p, weight: 5 })),
|
|
// Medium payloads (typical API responses)
|
|
{ json: mediumPayload10, weight: 8 },
|
|
{ json: mediumPayload50, weight: 6 },
|
|
// Large payloads (analytics, batch operations)
|
|
{ json: largePayload100, weight: 3 },
|
|
{ json: largePayload500, weight: 1 },
|
|
// Nested payloads (config, GraphQL)
|
|
{ json: nestedPayload, weight: 2 },
|
|
// Table payloads (data grids, reports)
|
|
{ json: tablePayload, weight: 2 },
|
|
// Unicode payloads (i18n)
|
|
{ json: UNICODE_PAYLOAD, weight: 2 },
|
|
];
|
|
|
|
const weightedPayloads = [];
|
|
for (const p of PAYLOADS) {
|
|
for (let i = 0; i < p.weight; i++) {
|
|
weightedPayloads.push(p.json);
|
|
}
|
|
}
|
|
|
|
// Workload 1: Parse → access properties → stringify (REST API middleware pattern)
|
|
function workloadParseTransformStringify(jsonStr) {
|
|
const obj = JSON.parse(jsonStr);
|
|
|
|
// Typical middleware transforms
|
|
if (obj.data && Array.isArray(obj.data)) {
|
|
// Add computed field (common API pattern)
|
|
for (const item of obj.data) {
|
|
item._computed =
|
|
typeof item.id === 'string' ? item.id.toUpperCase() : String(item.id);
|
|
}
|
|
}
|
|
if (obj.events && Array.isArray(obj.events)) {
|
|
// Filter/transform (analytics pipeline)
|
|
obj.events = obj.events.filter((e) => e.type !== 'error');
|
|
obj.filteredCount = obj.events.length;
|
|
}
|
|
|
|
return JSON.stringify(obj);
|
|
}
|
|
|
|
// Workload 2: Parse → extract subset → stringify (GraphQL resolver pattern)
|
|
function workloadSelectiveSerialize(jsonStr) {
|
|
const obj = JSON.parse(jsonStr);
|
|
const subset = {};
|
|
|
|
// Pick only requested fields (like GraphQL field selection)
|
|
for (const key of Object.keys(obj)) {
|
|
if (typeof obj[key] !== 'object' || obj[key] === null) {
|
|
subset[key] = obj[key];
|
|
} else if (Array.isArray(obj[key]) && obj[key].length > 0) {
|
|
subset[key] = obj[key].slice(0, 5).map((item) => {
|
|
if (typeof item === 'object' && item !== null) {
|
|
const { id, name, type, email } = item;
|
|
return { id, name, type, email };
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(subset);
|
|
}
|
|
|
|
// Workload 3: Repeated parse/stringify of same shape (template response caching pattern)
|
|
function workloadRepeatedShapes() {
|
|
const results = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
const obj = {
|
|
id: i,
|
|
name: `Item ${i}`,
|
|
description: `Description for item ${i} with some additional text`,
|
|
price: (i * 1.5).toFixed(2),
|
|
inStock: i % 3 !== 0,
|
|
categories: ['cat1', 'cat2'],
|
|
ratings: { average: 4.5, count: i * 10 },
|
|
};
|
|
results.push(JSON.stringify(obj));
|
|
}
|
|
// Parse them all back
|
|
return results.map((s) => JSON.parse(s));
|
|
}
|
|
|
|
// Workload 4: JSON.parse with reviver / JSON.stringify with replacer
|
|
function workloadReviverReplacer(jsonStr) {
|
|
// Parse with date reviver (common pattern)
|
|
const dateReviver = (key, value) => {
|
|
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
return new Date(value);
|
|
}
|
|
return value;
|
|
};
|
|
const obj = JSON.parse(jsonStr, dateReviver);
|
|
|
|
// Stringify with replacer to exclude nulls (sanitization)
|
|
const nullRemover = (key, value) => (value === null ? undefined : value);
|
|
return JSON.stringify(obj, nullRemover);
|
|
}
|
|
|
|
// Workload 5: Build large JSON incrementally (logging, audit trail)
|
|
function workloadIncrementalBuild() {
|
|
const entries = [];
|
|
for (let i = 0; i < 200; i++) {
|
|
entries.push({
|
|
level: ['info', 'warn', 'error', 'debug'][i % 4],
|
|
message: `Log entry ${i}: Operation completed successfully with result code ${i * 3}`,
|
|
timestamp: Date.now() - i * 1000,
|
|
context: { requestId: `req_${i}`, userId: `user_${i % 20}` },
|
|
});
|
|
}
|
|
const logBatch = JSON.stringify({ entries, count: entries.length });
|
|
// Verify round-trip
|
|
const parsed = JSON.parse(logBatch);
|
|
return parsed.count;
|
|
}
|
|
|
|
async function main() {
|
|
console.log('[pgo-json] Starting JSON processing workload...');
|
|
const startTime = Date.now();
|
|
let totalOps = 0;
|
|
let batchNum = 0;
|
|
|
|
while (Date.now() - startTime < DURATION_MS) {
|
|
batchNum++;
|
|
|
|
// Workload 1: Parse-transform-stringify (highest weight — most common)
|
|
for (let i = 0; i < ITERATIONS_PER_BATCH; i++) {
|
|
const payload = weightedPayloads[i % weightedPayloads.length];
|
|
workloadParseTransformStringify(payload);
|
|
totalOps++;
|
|
}
|
|
|
|
// Workload 2: Selective serialization
|
|
for (let i = 0; i < Math.floor(ITERATIONS_PER_BATCH / 2); i++) {
|
|
const payload = weightedPayloads[i % weightedPayloads.length];
|
|
workloadSelectiveSerialize(payload);
|
|
totalOps++;
|
|
}
|
|
|
|
// Workload 3: Repeated shapes (every 3rd batch)
|
|
if (batchNum % 3 === 0) {
|
|
workloadRepeatedShapes();
|
|
totalOps += 200; // 100 stringify + 100 parse
|
|
}
|
|
|
|
// Workload 4: Reviver/replacer (every 2nd batch)
|
|
if (batchNum % 2 === 0) {
|
|
for (let i = 0; i < 50; i++) {
|
|
const payload = weightedPayloads[i % weightedPayloads.length];
|
|
workloadReviverReplacer(payload);
|
|
totalOps++;
|
|
}
|
|
}
|
|
|
|
// Workload 5: Incremental build (every 5th batch)
|
|
if (batchNum % 5 === 0) {
|
|
workloadIncrementalBuild();
|
|
totalOps += 201; // 200 items + 1 round-trip
|
|
}
|
|
|
|
// Yield to event loop periodically (realistic for servers)
|
|
if (batchNum % 10 === 0) {
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
}
|
|
}
|
|
|
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
console.log(
|
|
`[pgo-json] Completed ${totalOps} JSON operations in ${elapsed.toFixed(1)}s (${(totalOps / elapsed).toFixed(0)} ops/s)`,
|
|
);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('[pgo-json] Error:', err);
|
|
process.exit(1);
|
|
});
|