Created
September 23, 2025 08:40
-
-
Save corporatepiyush/9c53b49f2735c3f5885bfa32b7c16bc9 to your computer and use it in GitHub Desktop.
Benchmark for JSON vs Binary and why JS/V8 so stupid
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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