Spaces:
Configuration error
Configuration error
; | |
/** | |
* @author jdiaz5513 | |
*/ | |
Object.defineProperty(exports, "__esModule", { value: true }); | |
exports.unpack = exports.pack = exports.getZeroByteCount = exports.getUnpackedByteLength = exports.getTagByte = exports.getHammingWeight = void 0; | |
const constants_1 = require("../constants"); | |
const errors_1 = require("../errors"); | |
/** | |
* Compute the Hamming weight (number of bits set to 1) of a number. Used to figure out how many bytes follow a tag byte | |
* while computing the size of a packed message. | |
* | |
* WARNING: Using this with floating point numbers will void your warranty. | |
* | |
* @param {number} x A real integer. | |
* @returns {number} The hamming weight (integer). | |
*/ | |
function getHammingWeight(x) { | |
// Thanks, HACKMEM! | |
let w = x - ((x >> 1) & 0x55555555); | |
w = (w & 0x33333333) + ((w >> 2) & 0x33333333); | |
return (((w + (w >> 4)) & 0x0f0f0f0f) * 0x01010101) >> 24; | |
} | |
exports.getHammingWeight = getHammingWeight; | |
/** | |
* Compute the tag byte from the 8 bytes of a 64-bit word. | |
* | |
* @param {byte} a The first byte. | |
* @param {byte} b The second byte. | |
* @param {byte} c The third byte. | |
* @param {byte} d The fourth byte. | |
* @param {byte} e The fifth byte. | |
* @param {byte} f The sixth byte. | |
* @param {byte} g The seventh byte. | |
* @param {byte} h The eighth byte (phew!). | |
* @returns {number} The tag byte. | |
*/ | |
function getTagByte(a, b, c, d, e, f, g, h) { | |
// Yes, it's pretty. Don't touch it. | |
return ((a === 0 ? 0 : 0b00000001) | | |
(b === 0 ? 0 : 0b00000010) | | |
(c === 0 ? 0 : 0b00000100) | | |
(d === 0 ? 0 : 0b00001000) | | |
(e === 0 ? 0 : 0b00010000) | | |
(f === 0 ? 0 : 0b00100000) | | |
(g === 0 ? 0 : 0b01000000) | | |
(h === 0 ? 0 : 0b10000000)); | |
} | |
exports.getTagByte = getTagByte; | |
/** | |
* Efficiently calculate the length of a packed Cap'n Proto message. | |
* | |
* @export | |
* @param {ArrayBuffer} packed The packed message. | |
* @returns {number} The length of the unpacked message in bytes. | |
*/ | |
function getUnpackedByteLength(packed) { | |
const p = new Uint8Array(packed); | |
let wordLength = 0; | |
let lastTag = 0x77; | |
for (let i = 0; i < p.byteLength;) { | |
const tag = p[i]; | |
if (lastTag === 0 /* ZERO */) { | |
wordLength += tag; | |
i++; | |
lastTag = 0x77; | |
} | |
else if (lastTag === 255 /* SPAN */) { | |
wordLength += tag; | |
i += tag * 8 + 1; | |
lastTag = 0x77; | |
} | |
else { | |
wordLength++; | |
i += getHammingWeight(tag) + 1; | |
lastTag = tag; | |
} | |
} | |
return wordLength * 8; | |
} | |
exports.getUnpackedByteLength = getUnpackedByteLength; | |
/** | |
* Compute the number of zero bytes that occur in a given 64-bit word, provided as eight separate bytes. | |
* | |
* @param {byte} a The first byte. | |
* @param {byte} b The second byte. | |
* @param {byte} c The third byte. | |
* @param {byte} d The fourth byte. | |
* @param {byte} e The fifth byte. | |
* @param {byte} f The sixth byte. | |
* @param {byte} g The seventh byte. | |
* @param {byte} h The eighth byte (phew!). | |
* @returns {number} The number of these bytes that are zero. | |
*/ | |
function getZeroByteCount(a, b, c, d, e, f, g, h) { | |
return ((a === 0 ? 1 : 0) + | |
(b === 0 ? 1 : 0) + | |
(c === 0 ? 1 : 0) + | |
(d === 0 ? 1 : 0) + | |
(e === 0 ? 1 : 0) + | |
(f === 0 ? 1 : 0) + | |
(g === 0 ? 1 : 0) + | |
(h === 0 ? 1 : 0)); | |
} | |
exports.getZeroByteCount = getZeroByteCount; | |
/** | |
* Pack a section of a Cap'n Proto message into a compressed format. This will efficiently compress zero bytes (which | |
* are common in idiomatic Cap'n Proto messages) into a compact form. | |
* | |
* For stream-framed messages this is called once for the frame header and once again for each segment in the message. | |
* | |
* The returned array buffer is trimmed to the exact size of the packed message with a single copy operation at the end. | |
* This should be decent on CPU time but does require quite a lot of memory (a normal array is filled up with each | |
* packed byte until the packing is complete). | |
* | |
* @export | |
* @param {ArrayBuffer} unpacked The message to pack. | |
* @param {number} [byteOffset] Starting byte offset to read bytes from, defaults to 0. | |
* @param {number} [byteLength] Total number of bytes to read, defaults to the remainder of the buffer contents. | |
* @returns {ArrayBuffer} A packed version of the message. | |
*/ | |
function pack(unpacked, byteOffset = 0, byteLength) { | |
if (unpacked.byteLength % 8 !== 0) | |
throw new Error(errors_1.MSG_PACK_NOT_WORD_ALIGNED); | |
const src = new Uint8Array(unpacked, byteOffset, byteLength); | |
// TODO: Maybe we should do this with buffers? This costs more than 8x the final compressed size in temporary RAM. | |
const dst = []; | |
/* Just have to be sure it's neither ZERO nor SPAN. */ | |
let lastTag = 0x77; | |
/** This is where we need to remember to write the SPAN tag (0xff). */ | |
let spanTagOffset = NaN; | |
/** How many words have been copied during the current span. */ | |
let spanWordLength = 0; | |
/** | |
* When this hits zero, we've had PACK_SPAN_THRESHOLD zero bytes pass by and it's time to bail from the span. | |
*/ | |
let spanThreshold = constants_1.PACK_SPAN_THRESHOLD; | |
for (let srcByteOffset = 0; srcByteOffset < src.byteLength; srcByteOffset += 8) { | |
/** Read in the entire word. Yes, this feels silly but it's fast! */ | |
const a = src[srcByteOffset]; | |
const b = src[srcByteOffset + 1]; | |
const c = src[srcByteOffset + 2]; | |
const d = src[srcByteOffset + 3]; | |
const e = src[srcByteOffset + 4]; | |
const f = src[srcByteOffset + 5]; | |
const g = src[srcByteOffset + 6]; | |
const h = src[srcByteOffset + 7]; | |
const tag = getTagByte(a, b, c, d, e, f, g, h); | |
/** If this is true we'll skip the normal word write logic after the switch statement. */ | |
let skipWriteWord = true; | |
switch (lastTag) { | |
case 0 /* ZERO */: | |
// We're writing a span of words with all zeroes in them. See if we need to bail out of the fast path. | |
if (tag !== 0 /* ZERO */ || spanWordLength >= 0xff) { | |
// There's a bit in there or we got too many zeroes. Damn, we need to bail. | |
dst.push(spanWordLength); | |
spanWordLength = 0; | |
skipWriteWord = false; | |
} | |
else { | |
// Kay, let's quickly inc this and go. | |
spanWordLength++; | |
} | |
break; | |
case 255 /* SPAN */: { | |
// We're writing a span of nonzero words. | |
const zeroCount = getZeroByteCount(a, b, c, d, e, f, g, h); | |
// See if we need to bail now. | |
spanThreshold -= zeroCount; | |
if (spanThreshold <= 0 || spanWordLength >= 0xff) { | |
// Alright, time to get packing again. Write the number of words we skipped to the beginning of the span. | |
dst[spanTagOffset] = spanWordLength; | |
spanWordLength = 0; | |
spanThreshold = constants_1.PACK_SPAN_THRESHOLD; | |
// We have to write this word normally. | |
skipWriteWord = false; | |
} | |
else { | |
// Just write this word verbatim. | |
dst.push(a, b, c, d, e, f, g, h); | |
spanWordLength++; | |
} | |
break; | |
} | |
default: | |
// Didn't get a special tag last time, let's write this as normal. | |
skipWriteWord = false; | |
break; | |
} | |
// A goto is fast, idk why people keep hatin'. | |
if (skipWriteWord) | |
continue; | |
dst.push(tag); | |
lastTag = tag; | |
if (a !== 0) | |
dst.push(a); | |
if (b !== 0) | |
dst.push(b); | |
if (c !== 0) | |
dst.push(c); | |
if (d !== 0) | |
dst.push(d); | |
if (e !== 0) | |
dst.push(e); | |
if (f !== 0) | |
dst.push(f); | |
if (g !== 0) | |
dst.push(g); | |
if (h !== 0) | |
dst.push(h); | |
// Record the span tag offset if needed, making sure to actually leave room for it. | |
if (tag === 255 /* SPAN */) { | |
spanTagOffset = dst.length; | |
dst.push(0); | |
} | |
} | |
// We're done. If we were writing a span let's finish it. | |
if (lastTag === 0 /* ZERO */) { | |
dst.push(spanWordLength); | |
} | |
else if (lastTag === 255 /* SPAN */) { | |
dst[spanTagOffset] = spanWordLength; | |
} | |
return new Uint8Array(dst).buffer; | |
} | |
exports.pack = pack; | |
/** | |
* Unpack a compressed Cap'n Proto message into a new ArrayBuffer. | |
* | |
* Unlike the `pack` function, this is able to efficiently determine the exact size needed for the output buffer and | |
* runs considerably more efficiently. | |
* | |
* @export | |
* @param {ArrayBuffer} packed An array buffer containing the packed message. | |
* @returns {ArrayBuffer} The unpacked message. | |
*/ | |
function unpack(packed) { | |
// We have no choice but to read the packed buffer one byte at a time. | |
const src = new Uint8Array(packed); | |
const dst = new Uint8Array(new ArrayBuffer(getUnpackedByteLength(packed))); | |
/** The last tag byte that we've seen - it starts at a "neutral" value. */ | |
let lastTag = 0x77; | |
for (let srcByteOffset = 0, dstByteOffset = 0; srcByteOffset < src.byteLength;) { | |
const tag = src[srcByteOffset]; | |
if (lastTag === 0 /* ZERO */) { | |
// We have a span of zeroes. New array buffers are guaranteed to be initialized to zero so we just seek ahead. | |
dstByteOffset += tag * 8; | |
srcByteOffset++; | |
lastTag = 0x77; | |
} | |
else if (lastTag === 255 /* SPAN */) { | |
// We have a span of unpacked bytes. Copy them verbatim from the source buffer. | |
const spanByteLength = tag * 8; | |
dst.set(src.subarray(srcByteOffset + 1, srcByteOffset + 1 + spanByteLength), dstByteOffset); | |
dstByteOffset += spanByteLength; | |
srcByteOffset += 1 + spanByteLength; | |
lastTag = 0x77; | |
} | |
else { | |
// Okay, a normal tag. Let's read past the tag and copy bytes that have a bit set in the tag. | |
srcByteOffset++; | |
for (let i = 1; i <= 0b10000000; i <<= 1) { | |
// We only need to actually touch `dst` if there's a nonzero byte (it's already initialized to zeroes). | |
if ((tag & i) !== 0) | |
dst[dstByteOffset] = src[srcByteOffset++]; | |
dstByteOffset++; | |
} | |
lastTag = tag; | |
} | |
} | |
return dst.buffer; | |
} | |
exports.unpack = unpack; | |
//# sourceMappingURL=packing.js.map |