Skip to content

Instantly share code, notes, and snippets.

@corporatepiyush
Created September 23, 2025 08:40
Show Gist options
  • Save corporatepiyush/9c53b49f2735c3f5885bfa32b7c16bc9 to your computer and use it in GitHub Desktop.
Save corporatepiyush/9c53b49f2735c3f5885bfa32b7c16bc9 to your computer and use it in GitHub Desktop.
Benchmark for JSON vs Binary and why JS/V8 so stupid
// benchmark.js
import { faker } from '@faker-js/faker';
import fs from 'fs';
import crypto from 'crypto';
// Text encoder/decoder for binary operations
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// Mock MessageType enum
const MessageType = {
userProfile: 1
};
// Your UserProfile class with fair deserialization
class UserProfile {
static FIXED_SIZE = 1 + 4 + 20 + 8 + 8 + 1 + 1 + 4 + 4 + 32 + 10;
static USERNAME_OFFSET = 5;
static USERNAME_SIZE = 20;
static EMAIL_HASH_OFFSET = 51;
static EMAIL_HASH_SIZE = 32;
static THEME_OFFSET = 83;
static THEME_SIZE = 10;
constructor(buffer) {
this.buffer = buffer;
}
static create(
userId,
username,
registrationDate,
lastLoginDate,
isVerified,
isAdmin,
avatarId,
sessionCount,
emailHash,
preferredTheme,
) {
const buffer = new DataView(new ArrayBuffer(this.FIXED_SIZE));
buffer.setUint8(0, MessageType.userProfile);
buffer.setUint32(1, userId, true);
const usernameBytes = textEncoder.encode(
username.padEnd(this.USERNAME_SIZE, "\0"),
);
new Uint8Array(buffer.buffer).set(usernameBytes, this.USERNAME_OFFSET);
buffer.setBigUint64(25, BigInt(registrationDate), true);
buffer.setBigUint64(33, BigInt(lastLoginDate), true);
buffer.setUint8(41, isVerified ? 1 : 0);
buffer.setUint8(42, isAdmin ? 1 : 0);
buffer.setUint32(43, avatarId, true);
buffer.setUint32(47, sessionCount, true);
const emailHashBytes = textEncoder.encode(
emailHash.padEnd(this.EMAIL_HASH_SIZE, "\0"),
);
new Uint8Array(buffer.buffer).set(emailHashBytes, this.EMAIL_HASH_OFFSET);
const preferredThemeBytes = textEncoder.encode(
preferredTheme.padEnd(this.THEME_SIZE, "\0"),
);
new Uint8Array(buffer.buffer).set(preferredThemeBytes, this.THEME_OFFSET);
return new UserProfile(buffer);
}
get userId() {
return this.buffer.getUint32(1, true);
}
get username() {
const bytes = new Uint8Array(
this.buffer.buffer,
this.USERNAME_OFFSET,
this.USERNAME_SIZE,
);
return textDecoder.decode(bytes).replace(/\0/g, "");
}
get registrationDate() {
return Number(this.buffer.getBigUint64(25, true));
}
get lastLoginDate() {
return Number(this.buffer.getBigUint64(33, true));
}
get isVerified() {
return this.buffer.getUint8(41) !== 0;
}
get isAdmin() {
return this.buffer.getUint8(42) !== 0;
}
get avatarId() {
return this.buffer.getUint32(43, true);
}
get sessionCount() {
return this.buffer.getUint32(47, true);
}
get emailHash() {
const bytes = new Uint8Array(
this.buffer.buffer,
this.EMAIL_HASH_OFFSET,
this.EMAIL_HASH_SIZE,
);
return textDecoder.decode(bytes).replace(/\0/g, "");
}
get preferredTheme() {
const bytes = new Uint8Array(
this.buffer.buffer,
this.THEME_OFFSET,
this.THEME_SIZE,
);
return textDecoder.decode(bytes).replace(/\0/g, "");
}
toString() {
return `UserProfile(userId: ${this.userId}, username: ${this.username}, registrationDate: ${this.registrationDate}, lastLoginDate: ${this.lastLoginDate}, isVerified: ${this.isVerified}, isAdmin: ${this.isAdmin}, avatarId: ${this.avatarId}, sessionCount: ${this.sessionCount}, emailHash: ${this.emailHash}, preferredTheme: ${this.preferredTheme})`;
}
// Add serialization method for benchmarking
serialize() {
return new Uint8Array(this.buffer.buffer);
}
// Fair deserialization method that actually calls all getters
static deserialize(uint8Array) {
const buffer = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
const profile = new UserProfile(buffer);
// Actually access all properties through getters to make it fair
// This simulates real usage where you'd access the data
const data = {
userId: profile.userId,
username: profile.username,
registrationDate: profile.registrationDate,
lastLoginDate: profile.lastLoginDate,
isVerified: profile.isVerified,
isAdmin: profile.isAdmin,
avatarId: profile.avatarId,
sessionCount: profile.sessionCount,
emailHash: profile.emailHash,
preferredTheme: profile.preferredTheme
};
return data; // Return plain object like JSON.parse does
}
// Alternative: Return the UserProfile instance but ensure getters are called
static deserializeToInstance(uint8Array) {
const buffer = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);
const profile = new UserProfile(buffer);
// Call all getters to ensure fair comparison
profile.userId;
profile.username;
profile.registrationDate;
profile.lastLoginDate;
profile.isVerified;
profile.isAdmin;
profile.avatarId;
profile.sessionCount;
profile.emailHash;
profile.preferredTheme;
return profile;
}
}
// Generate test data using Faker.js
function generateUserProfileData(size) {
const data = [];
const themes = ['dark', 'light', 'auto', 'blue', 'green'];
for (let i = 0; i < size; i++) {
// Generate email hash (simulating a real hash)
const email = faker.internet.email();
const emailHash = crypto.createHash('md5').update(email).digest('hex');
const userData = {
userId: faker.number.int({ min: 1, max: 1000000 }),
username: faker.internet.username().substring(0, 19),
registrationDate: faker.date.past({ years: 5 }).getTime(),
lastLoginDate: faker.date.recent({ days: 30 }).getTime(),
isVerified: faker.datatype.boolean(),
isAdmin: faker.datatype.boolean({ probability: 0.1 }),
avatarId: faker.number.int({ min: 1, max: 9999 }),
sessionCount: faker.number.int({ min: 0, max: 10000 }),
emailHash: emailHash,
preferredTheme: faker.helpers.arrayElement(themes)
};
data.push(userData);
}
return data;
}
// Benchmark function
function benchmark(name, fn, iterations = 1) {
// Warm up
try {
fn();
} catch (e) {
// Ignore warm-up errors
}
const start = process.hrtime.bigint();
let result;
for (let i = 0; i < iterations; i++) {
result = fn();
}
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000; // Convert to milliseconds
return {
name,
duration,
avgDuration: duration / iterations,
result
};
}
// Calculate memory usage
function getMemoryUsage() {
const used = process.memoryUsage();
return {
rss: Math.round(used.rss / 1024 / 1024 * 100) / 100,
heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100,
heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100,
external: Math.round(used.external / 1024 / 1024 * 100) / 100
};
}
// Main benchmark execution
async function runBenchmarks() {
console.log('πŸš€ Starting Fair UserProfile Binary vs JSON Benchmark');
console.log('=====================================================\n');
const DATA_SIZE = 10000;
console.log(`πŸ“Š Generating ${DATA_SIZE} UserProfile records with Faker.js...`);
const testData = generateUserProfileData(DATA_SIZE);
console.log(`βœ… Generated ${testData.length} user profile records\n`);
const memoryBefore = getMemoryUsage();
console.log('πŸ’Ύ Memory usage before benchmarks:', memoryBefore, 'MB\n');
// Create UserProfile objects for binary serialization
console.log('πŸ”§ Creating binary UserProfile objects...');
const binaryProfiles = testData.map(data => UserProfile.create(
data.userId,
data.username,
data.registrationDate,
data.lastLoginDate,
data.isVerified,
data.isAdmin,
data.avatarId,
data.sessionCount,
data.emailHash,
data.preferredTheme
));
console.log('βœ… Binary UserProfile objects created\n');
// JSON Benchmarks
console.log('πŸ“‹ JSON Serialization/Deserialization:');
console.log('======================================');
let jsonSerialized;
const jsonSerializeResult = benchmark('JSON Serialize', () => {
jsonSerialized = JSON.stringify(testData);
return jsonSerialized;
});
const jsonDeserializeResult = benchmark('JSON Deserialize', () => {
return JSON.parse(jsonSerialized);
});
console.log(`✏️ JSON Serialize: ${jsonSerializeResult.avgDuration.toFixed(2)}ms`);
console.log(`πŸ“– JSON Deserialize: ${jsonDeserializeResult.avgDuration.toFixed(2)}ms`);
console.log(`πŸ“ JSON Size: ${jsonSerialized.length} characters (${(jsonSerialized.length / 1024).toFixed(2)} KB)\n`);
// Binary Benchmarks
console.log('πŸ”’ Binary UserProfile Serialization/Deserialization:');
console.log('===================================================');
let binarySerialized;
const binarySerializeResult = benchmark('Binary Serialize', () => {
binarySerialized = binaryProfiles.map(profile => profile.serialize());
return binarySerialized;
});
const binaryDeserializeResult = benchmark('Binary Deserialize (Fair)', () => {
return binarySerialized.map(bytes => UserProfile.deserialize(bytes));
});
console.log(`✏️ Binary Serialize: ${binarySerializeResult.avgDuration.toFixed(2)}ms`);
console.log(`πŸ“– Binary Deserialize: ${binaryDeserializeResult.avgDuration.toFixed(2)}ms`);
const binarySize = binarySerialized.reduce((total, bytes) => total + bytes.length, 0);
console.log(`πŸ“ Binary Size: ${binarySize} bytes (${(binarySize / 1024).toFixed(2)} KB)\n`);
// Additional test: Lazy vs Eager deserialization
console.log('⚑ Lazy vs Eager Deserialization Comparison:');
console.log('============================================');
const binaryLazyResult = benchmark('Binary Deserialize (Lazy)', () => {
return binarySerialized.map(bytes => UserProfile.deserializeToInstance(bytes));
});
console.log(`πŸ¦₯ Binary Deserialize (Lazy): ${binaryLazyResult.avgDuration.toFixed(2)}ms`);
console.log(`⚑ Binary Deserialize (Eager): ${binaryDeserializeResult.avgDuration.toFixed(2)}ms\n`);
// Individual UserProfile Creation Benchmark
console.log('πŸ‘€ Individual UserProfile Creation:');
console.log('===================================');
const singleUserData = testData[0];
const jsonCreateResult = benchmark('JSON Create Single Profile', () => {
return { ...singleUserData };
}, 1000);
const binaryCreateResult = benchmark('Binary Create Single Profile', () => {
return UserProfile.create(
singleUserData.userId,
singleUserData.username,
singleUserData.registrationDate,
singleUserData.lastLoginDate,
singleUserData.isVerified,
singleUserData.isAdmin,
singleUserData.avatarId,
singleUserData.sessionCount,
singleUserData.emailHash,
singleUserData.preferredTheme
);
}, 1000);
console.log(`✏️ JSON Create (avg): ${jsonCreateResult.avgDuration.toFixed(4)}ms`);
console.log(`πŸ”’ Binary Create (avg): ${binaryCreateResult.avgDuration.toFixed(4)}ms\n`);
// Property Access Benchmark (Now truly fair!)
console.log('πŸ” Property Access Performance:');
console.log('===============================');
const jsonProfile = testData[0];
const binaryProfile = binaryProfiles[0];
const jsonAccessResult = benchmark('JSON Property Access', () => {
const { userId, username, isVerified, sessionCount } = jsonProfile;
return userId + username.length + (isVerified ? 1 : 0) + sessionCount;
}, 10000);
const binaryAccessResult = benchmark('Binary Property Access', () => {
const userId = binaryProfile.userId;
const username = binaryProfile.username;
const isVerified = binaryProfile.isVerified;
const sessionCount = binaryProfile.sessionCount;
return userId + username.length + (isVerified ? 1 : 0) + sessionCount;
}, 10000);
console.log(`πŸ“‹ JSON Access (avg): ${jsonAccessResult.avgDuration.toFixed(6)}ms`);
console.log(`πŸ”’ Binary Access (avg): ${binaryAccessResult.avgDuration.toFixed(6)}ms\n`);
// Comparison and Analysis
console.log('πŸ“Š Performance Comparison:');
console.log('==========================');
const serializeSpeedup = jsonSerializeResult.avgDuration / binarySerializeResult.avgDuration;
const deserializeSpeedup = jsonDeserializeResult.avgDuration / binaryDeserializeResult.avgDuration;
const createSpeedup = jsonCreateResult.avgDuration / binaryCreateResult.avgDuration;
const accessSpeedup = jsonAccessResult.avgDuration / binaryAccessResult.avgDuration;
console.log(`πŸƒ Serialization: ${serializeSpeedup > 1 ? 'Binary' : 'JSON'} is ${Math.abs(serializeSpeedup).toFixed(2)}x ${serializeSpeedup > 1 ? 'faster' : 'slower'}`);
console.log(`πŸƒ Deserialization: ${deserializeSpeedup > 1 ? 'Binary' : 'JSON'} is ${Math.abs(deserializeSpeedup).toFixed(2)}x ${deserializeSpeedup > 1 ? 'faster' : 'slower'}`);
console.log(`πŸƒ Creation: ${createSpeedup > 1 ? 'Binary' : 'JSON'} is ${Math.abs(createSpeedup).toFixed(2)}x ${createSpeedup > 1 ? 'faster' : 'slower'}`);
console.log(`πŸƒ Property Access: ${accessSpeedup > 1 ? 'Binary' : 'JSON'} is ${Math.abs(accessSpeedup).toFixed(2)}x ${accessSpeedup > 1 ? 'faster' : 'slower'}`);
const sizeDifference = ((binarySize - jsonSerialized.length) / jsonSerialized.length) * 100;
console.log(`πŸ“¦ Size difference: Binary is ${Math.abs(sizeDifference).toFixed(2)}% ${sizeDifference > 0 ? 'larger' : 'smaller'} than JSON`);
console.log(`πŸ“ Size per record: JSON ~${(jsonSerialized.length / DATA_SIZE).toFixed(1)} bytes, Binary ${UserProfile.FIXED_SIZE} bytes\n`);
// Memory usage after
const memoryAfter = getMemoryUsage();
console.log('πŸ’Ύ Memory usage after benchmarks:', memoryAfter, 'MB');
console.log(`πŸ“ˆ Memory increase: ${(memoryAfter.heapUsed - memoryBefore.heapUsed).toFixed(2)} MB\n`);
// Data integrity check
console.log('πŸ” Data Integrity Check:');
console.log('========================');
const jsonDeserialized = JSON.parse(jsonSerialized);
const binaryDeserialized = binarySerialized.map(bytes => UserProfile.deserialize(bytes));
// Check first profile
const originalData = testData[0];
const jsonResult = jsonDeserialized[0];
const binaryResult = binaryDeserialized[0];
const jsonIntegrity = (
originalData.userId === jsonResult.userId &&
originalData.username === jsonResult.username &&
originalData.isVerified === jsonResult.isVerified
);
const binaryIntegrity = (
originalData.userId === binaryResult.userId &&
originalData.username === binaryResult.username &&
originalData.isVerified === binaryResult.isVerified
);
console.log(`βœ… JSON data integrity: ${jsonIntegrity ? 'PASSED' : 'FAILED'}`);
console.log(`βœ… Binary data integrity: ${binaryIntegrity ? 'PASSED' : 'FAILED'}`);
if (binaryIntegrity) {
console.log(` Sample: userId=${binaryResult.userId}, username="${binaryResult.username}"`);
}
// Performance Analysis Summary
console.log('\nπŸ“ Performance Analysis Summary:');
console.log('================================');
console.log('This benchmark now fairly compares:');
console.log('β€’ JSON: Native stringify/parse with direct object property access');
console.log('β€’ Binary: Custom serialization with getter-based property access');
console.log('β€’ Both deserialization methods now perform equivalent work');
console.log(`β€’ Binary provides ${UserProfile.FIXED_SIZE} bytes per record vs JSON's variable size`);
// Save results to file
const results = {
dataSize: DATA_SIZE,
timestamp: new Date().toISOString(),
fixedBinarySize: UserProfile.FIXED_SIZE,
fairComparison: true,
json: {
serialize: jsonSerializeResult.avgDuration,
deserialize: jsonDeserializeResult.avgDuration,
create: jsonCreateResult.avgDuration,
access: jsonAccessResult.avgDuration,
totalSize: jsonSerialized.length,
avgSizePerRecord: jsonSerialized.length / DATA_SIZE
},
binary: {
serialize: binarySerializeResult.avgDuration,
deserialize: binaryDeserializeResult.avgDuration,
deserializeLazy: binaryLazyResult.avgDuration,
create: binaryCreateResult.avgDuration,
access: binaryAccessResult.avgDuration,
totalSize: binarySize,
avgSizePerRecord: UserProfile.FIXED_SIZE
},
speedups: {
serialize: serializeSpeedup,
deserialize: deserializeSpeedup,
create: createSpeedup,
access: accessSpeedup
},
memory: {
before: memoryBefore,
after: memoryAfter
}
};
fs.writeFileSync('fair-userprofile-benchmark-results.json', JSON.stringify(results, null, 2));
console.log('\nπŸ’Ύ Detailed results saved to fair-userprofile-benchmark-results.json');
}
// Error handling
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// Run the benchmark
runBenchmarks().catch(console.error);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment