ieeeColors = ({
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
red: "#E74C3C",
green: "#27AE60",
purple: "#8E44AD",
blue: "#3498DB",
yellow: "#F1C40F",
darkGray: "#34495E",
lightGray: "#ECF0F1"
})
// Serialization format definitions
formats = [
{
id: "json",
name: "JSON",
description: "JavaScript Object Notation - Human-readable text format",
color: ieeeColors.orange,
schemaRequired: false,
humanReadable: true,
binaryFormat: false,
overhead: 1.0,
encodeSpeed: 0.8,
decodeSpeed: 0.9,
features: ["Human readable", "Widely supported", "Self-describing", "Large size"]
},
{
id: "msgpack",
name: "MessagePack",
description: "Efficient binary serialization like JSON but smaller and faster",
color: ieeeColors.teal,
schemaRequired: false,
humanReadable: false,
binaryFormat: true,
overhead: 0.6,
encodeSpeed: 0.4,
decodeSpeed: 0.35,
features: ["Binary compact", "Schema-less", "Fast", "Good IoT fit"]
},
{
id: "protobuf",
name: "Protocol Buffers",
description: "Google's language-neutral, platform-neutral serialization",
color: ieeeColors.blue,
schemaRequired: true,
humanReadable: false,
binaryFormat: true,
overhead: 0.4,
encodeSpeed: 0.3,
decodeSpeed: 0.25,
features: ["Very compact", "Schema required", "Fastest", "Type safety"]
},
{
id: "cbor",
name: "CBOR",
description: "Concise Binary Object Representation - Binary JSON for constrained devices",
color: ieeeColors.purple,
schemaRequired: false,
humanReadable: false,
binaryFormat: true,
overhead: 0.55,
encodeSpeed: 0.35,
decodeSpeed: 0.3,
features: ["IoT optimized", "Self-describing", "IETF standard", "CoAP native"]
},
{
id: "avro",
name: "Apache Avro",
description: "Row-oriented binary format with schema evolution support",
color: ieeeColors.green,
schemaRequired: true,
humanReadable: false,
binaryFormat: true,
overhead: 0.45,
encodeSpeed: 0.5,
decodeSpeed: 0.45,
features: ["Schema evolution", "Compact", "Hadoop ecosystem", "Dynamic typing"]
},
{
id: "binary",
name: "Raw Binary",
description: "Fixed-structure binary encoding with no metadata",
color: ieeeColors.red,
schemaRequired: true,
humanReadable: false,
binaryFormat: true,
overhead: 0.25,
encodeSpeed: 0.1,
decodeSpeed: 0.1,
features: ["Smallest size", "Fastest", "Fragile", "No flexibility"]
}
]
// Sample IoT message types
messageTypes = [
{
id: "sensor_telemetry",
name: "Sensor Telemetry (Simple)",
description: "Basic sensor reading with timestamp",
fields: [
{ name: "deviceId", type: "string", value: "sens-001" },
{ name: "temperature", type: "float", value: 23.5 },
{ name: "humidity", type: "int", value: 65 },
{ name: "timestamp", type: "int", value: 1704067200 }
]
},
{
id: "complex_nested",
name: "Complex Nested Object",
description: "Device with location and multiple sensors",
fields: [
{ name: "deviceId", type: "string", value: "gateway-01" },
{ name: "location.lat", type: "float", value: 37.7749 },
{ name: "location.lon", type: "float", value: -122.4194 },
{ name: "location.altitude", type: "int", value: 15 },
{ name: "sensors.temp", type: "float", value: 22.3 },
{ name: "sensors.humidity", type: "int", value: 58 },
{ name: "sensors.pressure", type: "float", value: 1013.25 },
{ name: "sensors.co2", type: "int", value: 420 },
{ name: "battery", type: "int", value: 87 },
{ name: "firmware", type: "string", value: "v2.1.0" },
{ name: "timestamp", type: "int", value: 1704067200 }
]
},
{
id: "array_readings",
name: "Array of Readings",
description: "Batch of sensor readings for efficient transmission",
fields: [
{ name: "deviceId", type: "string", value: "sensor-42" },
{ name: "readings[0]", type: "float", value: 23.1 },
{ name: "readings[1]", type: "float", value: 23.2 },
{ name: "readings[2]", type: "float", value: 23.4 },
{ name: "readings[3]", type: "float", value: 23.3 },
{ name: "readings[4]", type: "float", value: 23.5 },
{ name: "readings[5]", type: "float", value: 23.6 },
{ name: "readings[6]", type: "float", value: 23.4 },
{ name: "readings[7]", type: "float", value: 23.2 },
{ name: "interval_ms", type: "int", value: 1000 },
{ name: "start_time", type: "int", value: 1704067200 }
]
},
{
id: "constrained_device",
name: "Constrained Device (Minimal)",
description: "Ultra-compact message for LoRa/NB-IoT",
fields: [
{ name: "id", type: "int", value: 42 },
{ name: "t", type: "int", value: 235 },
{ name: "h", type: "int", value: 65 },
{ name: "b", type: "int", value: 87 }
]
},
{
id: "event_alert",
name: "Event/Alert Message",
description: "Alert with severity and context",
fields: [
{ name: "alertId", type: "string", value: "ALT-2024-001" },
{ name: "deviceId", type: "string", value: "sensor-zone-3" },
{ name: "severity", type: "int", value: 2 },
{ name: "type", type: "string", value: "threshold_exceeded" },
{ name: "value", type: "float", value: 85.7 },
{ name: "threshold", type: "float", value: 80.0 },
{ name: "message", type: "string", value: "Temperature exceeded safe limit" },
{ name: "timestamp", type: "int", value: 1704067200 }
]
}
]
// Controls
viewof selectedMessageType = Inputs.select(messageTypes, {
label: "Message Type",
format: d => d.name,
value: messageTypes[0]
})
viewof selectedFormats = Inputs.checkbox(formats, {
label: "Compare Formats",
format: d => d.name,
value: formats.slice(0, 4)
})
viewof showHexDump = Inputs.toggle({
label: "Show Hex Dump View",
value: true
})
viewof optimizeFor = Inputs.radio(["size", "speed", "compatibility"], {
label: "Optimize For",
value: "size"
})
// Calculate serialized sizes for each format
serializationResults = {
const results = [];
const fields = selectedMessageType.fields;
// Build the message object
const message = {};
fields.forEach(f => {
const parts = f.name.split('.');
let obj = message;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i].replace(/\[\d+\]/, '');
if (!obj[part]) obj[part] = parts[i].includes('[') ? [] : {};
obj = obj[part];
}
const lastPart = parts[parts.length - 1];
const arrayMatch = lastPart.match(/(\w+)\[(\d+)\]/);
if (arrayMatch) {
if (!obj[arrayMatch[1]]) obj[arrayMatch[1]] = [];
obj[arrayMatch[1]][parseInt(arrayMatch[2])] = f.value;
} else {
obj[lastPart] = f.value;
}
});
selectedFormats.forEach(format => {
let size = 0;
let hexBytes = [];
let encodeTime = 0;
let decodeTime = 0;
// Simulate serialization
switch (format.id) {
case "json":
const jsonStr = JSON.stringify(message);
size = jsonStr.length;
hexBytes = Array.from(jsonStr).map(c => c.charCodeAt(0).toString(16).padStart(2, '0'));
encodeTime = size * format.encodeSpeed * 0.01;
decodeTime = size * format.decodeSpeed * 0.01;
break;
case "msgpack":
// Simulate MessagePack encoding
size = Math.ceil(JSON.stringify(message).length * 0.6);
hexBytes = generateSimulatedHex(size, 0x80, 0xdf);
encodeTime = size * format.encodeSpeed * 0.01;
decodeTime = size * format.decodeSpeed * 0.01;
break;
case "protobuf":
// Simulate Protocol Buffers encoding (varint, field tags)
let pbSize = 0;
fields.forEach(f => {
pbSize += 1; // field tag
if (f.type === "string") pbSize += 1 + f.value.length;
else if (f.type === "float") pbSize += 4;
else if (f.type === "int") pbSize += getVarintSize(f.value);
});
size = pbSize;
hexBytes = generateProtobufHex(fields);
encodeTime = size * format.encodeSpeed * 0.01;
decodeTime = size * format.decodeSpeed * 0.01;
break;
case "cbor":
// Simulate CBOR encoding
size = Math.ceil(JSON.stringify(message).length * 0.55);
hexBytes = generateCborHex(fields);
encodeTime = size * format.encodeSpeed * 0.01;
decodeTime = size * format.decodeSpeed * 0.01;
break;
case "avro":
// Simulate Avro encoding (schema not included in payload)
let avroSize = 0;
fields.forEach(f => {
if (f.type === "string") avroSize += 1 + f.value.length;
else if (f.type === "float") avroSize += 4;
else if (f.type === "int") avroSize += getVarintSize(f.value);
});
size = avroSize;
hexBytes = generateSimulatedHex(size, 0x00, 0xff);
encodeTime = size * format.encodeSpeed * 0.01;
decodeTime = size * format.decodeSpeed * 0.01;
break;
case "binary":
// Raw binary - tightest possible encoding
let binSize = 0;
fields.forEach(f => {
if (f.type === "string") binSize += f.value.length; // no length prefix
else if (f.type === "float") binSize += 4; // IEEE 754
else if (f.type === "int") binSize += getMinBytes(f.value);
});
size = binSize;
hexBytes = generateBinaryHex(fields);
encodeTime = size * format.encodeSpeed * 0.01;
decodeTime = size * format.decodeSpeed * 0.01;
break;
}
results.push({
format: format,
size: size,
hexBytes: hexBytes,
encodeTime: encodeTime,
decodeTime: decodeTime,
efficiency: (1 - size / results[0]?.size || 0) * 100
});
});
// Calculate efficiency relative to JSON
const jsonResult = results.find(r => r.format.id === "json");
if (jsonResult) {
results.forEach(r => {
r.efficiency = jsonResult.size > 0 ? ((1 - r.size / jsonResult.size) * 100).toFixed(1) : 0;
r.sizeRatio = jsonResult.size > 0 ? (r.size / jsonResult.size).toFixed(2) : 1;
});
}
return results.sort((a, b) => a.size - b.size);
}
// Helper functions
getVarintSize = (value) => {
if (value < 128) return 1;
if (value < 16384) return 2;
if (value < 2097152) return 3;
if (value < 268435456) return 4;
return 5;
}
getMinBytes = (value) => {
if (value < 256) return 1;
if (value < 65536) return 2;
if (value < 16777216) return 3;
return 4;
}
generateSimulatedHex = (size, minByte, maxByte) => {
const bytes = [];
for (let i = 0; i < size; i++) {
const byte = Math.floor(Math.random() * (maxByte - minByte + 1)) + minByte;
bytes.push(byte.toString(16).padStart(2, '0'));
}
return bytes;
}
generateProtobufHex = (fields) => {
const bytes = [];
fields.forEach((f, i) => {
// Field tag (field number << 3 | wire type)
const wireType = f.type === "string" ? 2 : (f.type === "float" ? 5 : 0);
bytes.push(((i + 1) << 3 | wireType).toString(16).padStart(2, '0'));
if (f.type === "string") {
bytes.push(f.value.length.toString(16).padStart(2, '0'));
Array.from(f.value).forEach(c => bytes.push(c.charCodeAt(0).toString(16).padStart(2, '0')));
} else if (f.type === "float") {
// IEEE 754 float (simplified)
bytes.push('41', 'bc', '00', '00');
} else {
// Varint
let val = f.value;
while (val >= 128) {
bytes.push((val & 0x7f | 0x80).toString(16).padStart(2, '0'));
val >>>= 7;
}
bytes.push(val.toString(16).padStart(2, '0'));
}
});
return bytes;
}
generateCborHex = (fields) => {
const bytes = [];
// Map header
bytes.push((0xa0 + Math.min(fields.length, 23)).toString(16).padStart(2, '0'));
fields.forEach(f => {
// Text string key
const keyLen = f.name.length;
bytes.push((0x60 + Math.min(keyLen, 23)).toString(16).padStart(2, '0'));
Array.from(f.name).forEach(c => bytes.push(c.charCodeAt(0).toString(16).padStart(2, '0')));
// Value
if (f.type === "string") {
bytes.push((0x60 + Math.min(f.value.length, 23)).toString(16).padStart(2, '0'));
Array.from(f.value).slice(0, 4).forEach(c => bytes.push(c.charCodeAt(0).toString(16).padStart(2, '0')));
} else if (f.type === "float") {
bytes.push('fb'); // float64 marker
bytes.push('40', '37', '80', '00', '00', '00', '00', '00');
} else {
if (f.value < 24) {
bytes.push(f.value.toString(16).padStart(2, '0'));
} else {
bytes.push('18', f.value.toString(16).padStart(2, '0'));
}
}
});
return bytes.slice(0, 60); // Limit for display
}
generateBinaryHex = (fields) => {
const bytes = [];
fields.forEach(f => {
if (f.type === "string") {
Array.from(f.value).forEach(c => bytes.push(c.charCodeAt(0).toString(16).padStart(2, '0')));
} else if (f.type === "float") {
// Simplified float bytes
const floatBytes = Math.round(f.value * 10);
bytes.push((floatBytes >> 8 & 0xff).toString(16).padStart(2, '0'));
bytes.push((floatBytes & 0xff).toString(16).padStart(2, '0'));
} else {
let val = f.value;
const byteCount = getMinBytes(val);
for (let i = byteCount - 1; i >= 0; i--) {
bytes.push(((val >> (i * 8)) & 0xff).toString(16).padStart(2, '0'));
}
}
});
return bytes;
}
// Best format recommendation
recommendation = {
const sorted = [...serializationResults];
let best;
switch (optimizeFor) {
case "size":
best = sorted[0];
break;
case "speed":
best = sorted.reduce((a, b) =>
(a.encodeTime + a.decodeTime) < (b.encodeTime + b.decodeTime) ? a : b
);
break;
case "compatibility":
best = sorted.find(r => r.format.id === "json") ||
sorted.find(r => r.format.id === "cbor") ||
sorted[0];
break;
}
let reasoning = "";
switch (optimizeFor) {
case "size":
reasoning = `${best.format.name} produces the smallest output at ${best.size} bytes, saving ${best.efficiency}% compared to JSON.`;
break;
case "speed":
reasoning = `${best.format.name} offers the fastest encoding/decoding at ${(best.encodeTime + best.decodeTime).toFixed(2)}ms total.`;
break;
case "compatibility":
reasoning = `${best.format.name} provides the best balance of human readability and tool support.`;
break;
}
return { best, reasoning };
}
// Main visualization
html`
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
<!-- Recommendation Banner -->
<div style="background: linear-gradient(135deg, ${recommendation.best.format.color}, ${ieeeColors.navy}); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
<div style="display: flex; align-items: center; gap: 15px;">
<div style="background: white; color: ${recommendation.best.format.color}; width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold;">
${recommendation.best.format.name.charAt(0)}
</div>
<div style="flex: 1;">
<div style="font-size: 14px; opacity: 0.9;">Recommended for ${optimizeFor === "size" ? "minimum size" : optimizeFor === "speed" ? "maximum speed" : "best compatibility"}</div>
<div style="font-size: 24px; font-weight: bold;">${recommendation.best.format.name}</div>
<div style="font-size: 13px; opacity: 0.8; margin-top: 5px;">${recommendation.reasoning}</div>
</div>
<div style="text-align: right;">
<div style="font-size: 32px; font-weight: bold;">${recommendation.best.size} B</div>
<div style="font-size: 12px; opacity: 0.8;">serialized size</div>
</div>
</div>
</div>
<!-- Size Comparison Chart -->
<div style="background: white; border: 1px solid ${ieeeColors.gray}30; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
<h4 style="margin: 0 0 15px 0; color: ${ieeeColors.navy};">Size Comparison (bytes)</h4>
<div style="display: flex; flex-direction: column; gap: 10px;">
${serializationResults.map((r, i) => html`
<div style="display: flex; align-items: center; gap: 15px;">
<div style="width: 120px; font-weight: 500; color: ${r.format.color};">${r.format.name}</div>
<div style="flex: 1; background: ${ieeeColors.lightGray}; border-radius: 4px; height: 30px; overflow: hidden; position: relative;">
<div style="background: ${r.format.color}; height: 100%; width: ${(r.size / serializationResults[serializationResults.length - 1].size) * 100}%; transition: width 0.5s ease; display: flex; align-items: center; justify-content: flex-end; padding-right: 10px;">
<span style="color: white; font-weight: bold; font-size: 12px; text-shadow: 0 1px 2px rgba(0,0,0,0.3);">${r.size} bytes</span>
</div>
</div>
<div style="width: 80px; text-align: right; font-size: 13px; color: ${parseFloat(r.efficiency) > 0 ? ieeeColors.green : ieeeColors.gray};">
${parseFloat(r.efficiency) > 0 ? `-${r.efficiency}%` : 'baseline'}
</div>
</div>
`)}
</div>
</div>
<!-- Speed Comparison -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
<!-- Encode Time -->
<div style="background: white; border: 1px solid ${ieeeColors.gray}30; border-radius: 12px; padding: 20px;">
<h4 style="margin: 0 0 15px 0; color: ${ieeeColors.navy};">Encode Time (ms)</h4>
<div style="display: flex; flex-direction: column; gap: 8px;">
${serializationResults.sort((a, b) => a.encodeTime - b.encodeTime).map(r => html`
<div style="display: flex; align-items: center; gap: 10px;">
<div style="width: 90px; font-size: 12px; color: ${r.format.color}; font-weight: 500;">${r.format.name}</div>
<div style="flex: 1; background: ${ieeeColors.lightGray}; border-radius: 3px; height: 16px; overflow: hidden;">
<div style="background: ${r.format.color}; height: 100%; width: ${Math.min(100, r.encodeTime * 10)}%;"></div>
</div>
<div style="width: 50px; text-align: right; font-size: 11px; color: ${ieeeColors.darkGray};">${r.encodeTime.toFixed(2)}</div>
</div>
`)}
</div>
</div>
<!-- Decode Time -->
<div style="background: white; border: 1px solid ${ieeeColors.gray}30; border-radius: 12px; padding: 20px;">
<h4 style="margin: 0 0 15px 0; color: ${ieeeColors.navy};">Decode Time (ms)</h4>
<div style="display: flex; flex-direction: column; gap: 8px;">
${serializationResults.sort((a, b) => a.decodeTime - b.decodeTime).map(r => html`
<div style="display: flex; align-items: center; gap: 10px;">
<div style="width: 90px; font-size: 12px; color: ${r.format.color}; font-weight: 500;">${r.format.name}</div>
<div style="flex: 1; background: ${ieeeColors.lightGray}; border-radius: 3px; height: 16px; overflow: hidden;">
<div style="background: ${r.format.color}; height: 100%; width: ${Math.min(100, r.decodeTime * 10)}%;"></div>
</div>
<div style="width: 50px; text-align: right; font-size: 11px; color: ${ieeeColors.darkGray};">${r.decodeTime.toFixed(2)}</div>
</div>
`)}
</div>
</div>
</div>
<!-- Message Preview -->
<div style="background: ${ieeeColors.navy}; color: white; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
<h4 style="margin: 0 0 15px 0; color: ${ieeeColors.lightGray};">Message Structure: ${selectedMessageType.name}</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px;">
${selectedMessageType.fields.map(f => html`
<div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px;">
<div style="font-size: 11px; color: ${ieeeColors.teal}; margin-bottom: 3px;">${f.name}</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: bold;">${f.value}</span>
<span style="font-size: 10px; background: ${ieeeColors.gray}; padding: 2px 6px; border-radius: 3px;">${f.type}</span>
</div>
</div>
`)}
</div>
</div>
</div>
`