Lee Thanh
Upload 3012 files
5641073
raw
history blame
10.8 kB
"use strict";
/**
* @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