Last active
November 14, 2024 01:34
-
-
Save eliemichel/56d8013093b881c39e9b5de72f63e7e5 to your computer and use it in GitHub Desktop.
Javascript view builder to interpret raw ArrayBuffer data as structured data
This file contains 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
// Javascript view builder to interpret raw ArrayBuffer data as structured data. | |
// Copyright 2024 (c) Élie Michel - MIT Licensed | |
// NB: This is loosly inspired by https://github.com/kainino0x/structured-accessor | |
const kTypedArrayConstructors = { | |
i8: Int8Array, | |
u8: Uint8Array, | |
i16: Int16Array, | |
u16: Uint16Array, | |
i32: Int32Array, | |
u32: Uint32Array, | |
f32: Float32Array, | |
f64: Float64Array, | |
i64: BigInt64Array, | |
u64: BigUint64Array, | |
}; | |
class StructuredAccessorFactory { | |
// The 'layout' argument is quite low-level in this example, it is expected to be an object that | |
// gives for each key the type and byte offset. Nested structures are not examplified here but | |
// are in theory compatible with our approach. | |
constructor(layout) { | |
// We build the source code of the this.create() function, so that we can "prepare" this | |
// function once for all and thus makes the creation of a view as fast as possible. | |
// The preamble declares only the typed views that we need over the provided array buffer. | |
const preambleSrc = []; | |
const readyTypedArrays = new Set(); | |
const ensureTypedArray = (type) => { | |
if (!readyTypedArrays.has(type)) { | |
preambleSrc.push(`const data_as_${type} = new ${kTypedArrayConstructors[type].name}(buffer);`); | |
readyTypedArrays.add(type); | |
} | |
} | |
// The "kernel" is the core source of the pre-interpreted function. | |
const kernelSrc = []; | |
kernelSrc.push("let accessor = {};"); | |
for (const [ key, entry ] of Object.entries(layout)) { | |
const { byteOffset, type } = entry; | |
ensureTypedArray(type); | |
const arrayOffset = byteOffset / kTypedArrayConstructors[type].BYTES_PER_ELEMENT; | |
kernelSrc.push( | |
`Object.defineProperty(accessor, "${key}", {`, | |
` enumerable: true,`, | |
` get() { return data_as_${type}[${arrayOffset}]; },`, | |
` set(value) { data_as_${type}[${arrayOffset}] = value; },`, | |
`});`, | |
); | |
} | |
kernelSrc.push("return accessor;") | |
this.create = Function("buffer", [ ...preambleSrc, ...kernelSrc ].join("\n")); | |
} | |
} | |
// Tests | |
const ab = new ArrayBuffer(32); | |
const _0 = new StructuredAccessorFactory({ | |
value: { type: 'i32', byteOffset: 0 }, | |
}).create(ab); | |
const _1 = new StructuredAccessorFactory([ | |
{ name: '0', type: 'i32', byteOffset: 0 }, | |
{ name: '1', type: 'i32', byteOffset: 4 }, | |
]).create(ab); | |
const _2 = new StructuredAccessorFactory({ | |
x: { type: 'i8', byteOffset: 0 }, | |
y: { type: 'i32', byteOffset: 4 }, | |
z: { type: 'i8', byteOffset: 8 }, | |
}).create(ab); | |
console.log('_0 == ' + JSON.stringify(_0)); | |
console.log('_0.value == ' + JSON.stringify(_0.value)); | |
console.log(' setting _0.value'); | |
_0.value = 99; | |
console.log(' _0.value == ' + JSON.stringify(_0.value)); | |
console.log('arraybuffer = ' + new Int32Array(ab).toString()); | |
console.log('_1[0] == ' + JSON.stringify(_1[0])); | |
_1[1] = 12; | |
console.log('_1[1] == ' + JSON.stringify(_1[1])); | |
console.log('_2.x == ' + JSON.stringify(_2.x)); | |
console.log('_2.y == ' + JSON.stringify(_2.y)); | |
console.log('_2.z == ' + JSON.stringify(_2.z)); | |
console.log('arraybuffer = ' + new Int32Array(ab).toString()); | |
// nodejs perf test | |
const { performance } = require('perf_hooks'); | |
const factory = new StructuredAccessorFactory({ | |
x: { type: 'i32', byteOffset: 0 }, | |
y: { type: 'i32', byteOffset: 4 }, | |
z: { type: 'i32', byteOffset: 8 }, | |
}); | |
const buffer = new ArrayBuffer(32); | |
const begin = performance.now(); | |
for (let i = 0 ; i < 100000 ; ++i) { | |
const accessor = factory.create(buffer); | |
accessor.x = 42; | |
accessor.y = -314; | |
accessor.z = 3615; | |
//console.assert(accessor.x == 42); | |
//console.assert(accessor.y == -314); | |
//console.assert(accessor.z == 3615); | |
} | |
const end = performance.now(); | |
console.log(`elapsed: ${end-begin} ms`) | |
// elapsed: (220 ms w/o assertions) |
This file contains 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
// Same behavior as the previous script, only this one does not use Function, which leads to lower speed | |
const kTypedArrayConstructors = { | |
i8: Int8Array, | |
u8: Uint8Array, | |
i16: Int16Array, | |
u16: Uint16Array, | |
i32: Int32Array, | |
u32: Uint32Array, | |
f32: Float32Array, | |
f64: Float64Array, | |
i64: BigInt64Array, | |
u64: BigUint64Array, | |
}; | |
class StructuredAccessorFactory { | |
// The 'layout' argument is quite low-level in this example, it is expected to be an object that | |
// gives for each key the type and byte offset. Nested structures are not examplified here but | |
// are in theory compatible with our approach. | |
constructor(layout) { | |
this.layout = layout; | |
} | |
create(buffer) { | |
let accessor = {}; | |
for (const [ key, entry ] of Object.entries(this.layout)) { | |
const { byteOffset, type } = entry; | |
const typed_data = new kTypedArrayConstructors[type](buffer); | |
const arrayOffset = byteOffset / kTypedArrayConstructors[type].BYTES_PER_ELEMENT; | |
Object.defineProperty(accessor, key, { | |
enumerable: true, | |
get() { return typed_data[arrayOffset]; }, | |
set(value) { typed_data[arrayOffset] = value; }, | |
}); | |
} | |
return accessor; | |
} | |
} | |
// Tests | |
const ab = new ArrayBuffer(32); | |
const _0 = new StructuredAccessorFactory({ | |
value: { type: 'i32', byteOffset: 0 }, | |
}).create(ab); | |
const _1 = new StructuredAccessorFactory([ | |
{ name: '0', type: 'i32', byteOffset: 0 }, | |
{ name: '1', type: 'i32', byteOffset: 4 }, | |
]).create(ab); | |
const _2 = new StructuredAccessorFactory({ | |
x: { type: 'i8', byteOffset: 0 }, | |
y: { type: 'i32', byteOffset: 4 }, | |
z: { type: 'i8', byteOffset: 8 }, | |
}).create(ab); | |
console.log('_0 == ' + JSON.stringify(_0)); | |
console.log('_0.value == ' + JSON.stringify(_0.value)); | |
console.log(' setting _0.value'); | |
_0.value = 99; | |
console.log(' _0.value == ' + JSON.stringify(_0.value)); | |
console.log('arraybuffer = ' + new Int32Array(ab).toString()); | |
console.log('_1[0] == ' + JSON.stringify(_1[0])); | |
_1[1] = 12; | |
console.log('_1[1] == ' + JSON.stringify(_1[1])); | |
console.log('_2.x == ' + JSON.stringify(_2.x)); | |
console.log('_2.y == ' + JSON.stringify(_2.y)); | |
console.log('_2.z == ' + JSON.stringify(_2.z)); | |
console.log('arraybuffer = ' + new Int32Array(ab).toString()); | |
// nodejs perf test | |
const { performance } = require('perf_hooks'); | |
const factory = new StructuredAccessorFactory({ | |
x: { type: 'i32', byteOffset: 0 }, | |
y: { type: 'i32', byteOffset: 4 }, | |
z: { type: 'i32', byteOffset: 8 }, | |
}); | |
const buffer = new ArrayBuffer(32); | |
const begin = performance.now(); | |
for (let i = 0 ; i < 100000 ; ++i) { | |
const accessor = factory.create(buffer); | |
accessor.x = 42; | |
accessor.y = -314; | |
accessor.z = 3615; | |
//console.assert(accessor.x == 42); | |
//console.assert(accessor.y == -314); | |
//console.assert(accessor.z == 3615); | |
} | |
const end = performance.now(); | |
console.log(`elapsed: ${end-begin} ms`) | |
// elapsed: (340 ms w/o assertions) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment