310 lines
13 KiB
JavaScript
310 lines
13 KiB
JavaScript
import { MASKS, DEFAULT_PAGE_SIZE, BLOCK_SIZE, greatestMultiple, roundUpToMultipleOf32, normalizePageSize, } from "../decoding/fastPforShared";
|
|
const EXCEPTION_OVERHEAD_BITS = 8;
|
|
const MAX_BIT_WIDTH = 32;
|
|
const BIT_WIDTH_SLOTS = MAX_BIT_WIDTH + 1;
|
|
const PAGE_SIZE = normalizePageSize(DEFAULT_PAGE_SIZE);
|
|
const INITIAL_PACKED_BUFFER_SIZE_WORDS = (PAGE_SIZE / 32) * 4;
|
|
const BYTE_CONTAINER_SIZE = ((3 * PAGE_SIZE) / BLOCK_SIZE + PAGE_SIZE) | 0;
|
|
function requiredBits(value) {
|
|
return 32 - Math.clz32(value >>> 0);
|
|
}
|
|
function ensureInt32Capacity(buffer, requiredLength) {
|
|
if (requiredLength <= buffer.length)
|
|
return buffer;
|
|
let newLength = buffer.length === 0 ? 1 : buffer.length;
|
|
while (newLength < requiredLength) {
|
|
newLength *= 2;
|
|
}
|
|
const next = new Uint32Array(newLength);
|
|
next.set(buffer);
|
|
return next;
|
|
}
|
|
function ensureUint8Capacity(buffer, requiredLength) {
|
|
if (requiredLength <= buffer.length)
|
|
return buffer;
|
|
let newLength = buffer.length === 0 ? 1 : buffer.length;
|
|
while (newLength < requiredLength) {
|
|
newLength *= 2;
|
|
}
|
|
const next = new Uint8Array(newLength);
|
|
next.set(buffer);
|
|
return next;
|
|
}
|
|
export function fastPack32(inValues, inPos, out, outPos, bitWidth) {
|
|
if (bitWidth === 0)
|
|
return;
|
|
if (bitWidth === 32) {
|
|
out.set(inValues.subarray(inPos, inPos + 32), outPos);
|
|
return;
|
|
}
|
|
const mask = MASKS[bitWidth] >>> 0;
|
|
let outputWordIndex = outPos;
|
|
let bitOffset = 0;
|
|
let currentWord = 0;
|
|
for (let i = 0; i < 32; i++) {
|
|
const value = (inValues[inPos + i] >>> 0) & mask;
|
|
if (bitOffset + bitWidth <= 32) {
|
|
currentWord |= value << bitOffset;
|
|
bitOffset += bitWidth;
|
|
if (bitOffset === 32) {
|
|
out[outputWordIndex++] = currentWord | 0;
|
|
bitOffset = 0;
|
|
currentWord = 0;
|
|
}
|
|
}
|
|
else {
|
|
const lowBits = 32 - bitOffset;
|
|
const lowMask = MASKS[lowBits] >>> 0;
|
|
currentWord |= (value & lowMask) << bitOffset;
|
|
out[outputWordIndex++] = currentWord | 0;
|
|
currentWord = value >>> lowBits;
|
|
bitOffset = bitWidth - lowBits;
|
|
}
|
|
}
|
|
}
|
|
export function createFastPforEncoderWorkspace() {
|
|
const dataToBePacked = new Array(BIT_WIDTH_SLOTS);
|
|
for (let k = 1; k < BIT_WIDTH_SLOTS; k++) {
|
|
dataToBePacked[k] = new Uint32Array(INITIAL_PACKED_BUFFER_SIZE_WORDS);
|
|
}
|
|
return {
|
|
dataToBePacked,
|
|
dataPointers: new Int32Array(BIT_WIDTH_SLOTS),
|
|
byteContainer: new Uint8Array(BYTE_CONTAINER_SIZE),
|
|
bitWidthFrequencies: new Int32Array(BIT_WIDTH_SLOTS),
|
|
bestBitWidthPlan: new Int32Array(3),
|
|
};
|
|
}
|
|
function computeBestBitWidthPlan(inValues, pos, workspace) {
|
|
const bitWidthFrequencies = workspace.bitWidthFrequencies;
|
|
const bestBitWidthPlan = workspace.bestBitWidthPlan;
|
|
bitWidthFrequencies.fill(0);
|
|
for (let k = pos, kEnd = pos + BLOCK_SIZE; k < kEnd; k++) {
|
|
bitWidthFrequencies[requiredBits(inValues[k])]++;
|
|
}
|
|
let maxBitWidth = MAX_BIT_WIDTH;
|
|
while (bitWidthFrequencies[maxBitWidth] === 0)
|
|
maxBitWidth--;
|
|
let bestBitWidth = maxBitWidth;
|
|
let bestCost = maxBitWidth * BLOCK_SIZE;
|
|
let exceptionCount = 0;
|
|
let bestExceptionCount = exceptionCount;
|
|
for (let candidateBitWidth = maxBitWidth - 1; candidateBitWidth >= 0; candidateBitWidth--) {
|
|
exceptionCount += bitWidthFrequencies[candidateBitWidth + 1];
|
|
if (exceptionCount === BLOCK_SIZE)
|
|
break;
|
|
let candidateCost = exceptionCount * EXCEPTION_OVERHEAD_BITS +
|
|
exceptionCount * (maxBitWidth - candidateBitWidth) +
|
|
candidateBitWidth * BLOCK_SIZE +
|
|
8;
|
|
if (maxBitWidth - candidateBitWidth === 1)
|
|
candidateCost -= exceptionCount;
|
|
if (candidateCost < bestCost) {
|
|
bestCost = candidateCost;
|
|
bestBitWidth = candidateBitWidth;
|
|
bestExceptionCount = exceptionCount;
|
|
}
|
|
}
|
|
bestBitWidthPlan[0] = bestBitWidth;
|
|
bestBitWidthPlan[1] = bestExceptionCount;
|
|
bestBitWidthPlan[2] = maxBitWidth;
|
|
}
|
|
function writeByte(workspace, byteContainerPos, byteValue) {
|
|
if (byteContainerPos >= workspace.byteContainer.length) {
|
|
workspace.byteContainer = ensureUint8Capacity(workspace.byteContainer, byteContainerPos + 1);
|
|
}
|
|
workspace.byteContainer[byteContainerPos] = byteValue & 0xff;
|
|
return byteContainerPos + 1;
|
|
}
|
|
function ensureExceptionValuesCapacity(dataToBePacked, dataPointers, exceptionBitWidth, exceptionCount) {
|
|
if (exceptionBitWidth === 1)
|
|
return;
|
|
const needed = dataPointers[exceptionBitWidth] + exceptionCount;
|
|
const currentExceptionValues = dataToBePacked[exceptionBitWidth];
|
|
if (!currentExceptionValues || needed >= currentExceptionValues.length) {
|
|
let newSize = 2 * needed;
|
|
newSize = roundUpToMultipleOf32(newSize);
|
|
const next = new Uint32Array(newSize);
|
|
if (currentExceptionValues)
|
|
next.set(currentExceptionValues);
|
|
dataToBePacked[exceptionBitWidth] = next;
|
|
}
|
|
}
|
|
function writeBlockHeader(workspace, byteContainerPos, bitWidth, exceptionCount, maxBitWidth) {
|
|
byteContainerPos = writeByte(workspace, byteContainerPos, bitWidth);
|
|
byteContainerPos = writeByte(workspace, byteContainerPos, exceptionCount);
|
|
if (exceptionCount > 0) {
|
|
byteContainerPos = writeByte(workspace, byteContainerPos, maxBitWidth);
|
|
}
|
|
return byteContainerPos;
|
|
}
|
|
function recordBlockExceptions(workspace, inValues, blockPos, bitWidth, exceptionCount, exceptionBitWidth, byteContainerPos) {
|
|
if (exceptionCount === 0)
|
|
return byteContainerPos;
|
|
const dataToBePacked = workspace.dataToBePacked;
|
|
const dataPointers = workspace.dataPointers;
|
|
ensureExceptionValuesCapacity(dataToBePacked, dataPointers, exceptionBitWidth, exceptionCount);
|
|
for (let k = 0; k < BLOCK_SIZE; k++) {
|
|
const value = inValues[blockPos + k] >>> 0;
|
|
if (value >>> bitWidth !== 0) {
|
|
byteContainerPos = writeByte(workspace, byteContainerPos, k);
|
|
if (exceptionBitWidth !== 1) {
|
|
const exceptionValues = dataToBePacked[exceptionBitWidth];
|
|
exceptionValues[dataPointers[exceptionBitWidth]++] = (value >>> bitWidth) | 0;
|
|
}
|
|
}
|
|
}
|
|
return byteContainerPos;
|
|
}
|
|
function packBlock(inValues, blockPos, bitWidth, state) {
|
|
for (let k = 0; k < BLOCK_SIZE; k += 32) {
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + bitWidth);
|
|
fastPack32(inValues, blockPos + k, state.out, state.outPos, bitWidth);
|
|
state.outPos += bitWidth;
|
|
}
|
|
}
|
|
function padByteContainerToInt32(workspace, byteContainerPos) {
|
|
while ((byteContainerPos & 3) !== 0) {
|
|
byteContainerPos = writeByte(workspace, byteContainerPos, 0);
|
|
}
|
|
return byteContainerPos;
|
|
}
|
|
function writeByteContainerInts(workspace, state, byteContainerPos) {
|
|
const howManyInts = byteContainerPos / 4;
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + howManyInts);
|
|
const byteContainer = workspace.byteContainer;
|
|
for (let i = 0; i < howManyInts; i++) {
|
|
const base = i * 4;
|
|
const packedWord = byteContainer[base] |
|
|
(byteContainer[base + 1] << 8) |
|
|
(byteContainer[base + 2] << 16) |
|
|
(byteContainer[base + 3] << 24) |
|
|
0;
|
|
state.out[state.outPos + i] = packedWord;
|
|
}
|
|
state.outPos += howManyInts;
|
|
}
|
|
function computeExceptionBitmap(dataPointers) {
|
|
let bitmap = 0;
|
|
for (let k = 2; k <= MAX_BIT_WIDTH; k++) {
|
|
if (dataPointers[k] !== 0) {
|
|
bitmap |= k === MAX_BIT_WIDTH ? 0x80000000 : 1 << (k - 1);
|
|
}
|
|
}
|
|
return bitmap;
|
|
}
|
|
function writeExceptionStreams(workspace, state) {
|
|
const dataPointers = workspace.dataPointers;
|
|
const dataToBePacked = workspace.dataToBePacked;
|
|
const bitmap = computeExceptionBitmap(dataPointers);
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + 1);
|
|
state.out[state.outPos++] = bitmap;
|
|
for (let k = 2; k <= MAX_BIT_WIDTH; k++) {
|
|
const size = dataPointers[k];
|
|
if (size !== 0) {
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + 1);
|
|
state.out[state.outPos++] = size | 0;
|
|
let j = 0;
|
|
for (; j < size; j += 32) {
|
|
const exceptionValues = dataToBePacked[k];
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + k);
|
|
fastPack32(exceptionValues, j, state.out, state.outPos, k);
|
|
state.outPos += k;
|
|
}
|
|
const overflow = j - size;
|
|
state.outPos -= (overflow * k) >>> 5;
|
|
}
|
|
}
|
|
}
|
|
function encodePage(inValues, thisSize, state, workspace) {
|
|
const headerPos = state.outPos;
|
|
state.out = ensureInt32Capacity(state.out, headerPos + 1);
|
|
state.outPos = (state.outPos + 1) | 0;
|
|
const dataPointers = workspace.dataPointers;
|
|
dataPointers.fill(0);
|
|
let byteContainerPos = 0;
|
|
let tmpInPos = state.inPos;
|
|
const finalInPos = tmpInPos + thisSize - BLOCK_SIZE;
|
|
for (; tmpInPos <= finalInPos; tmpInPos += BLOCK_SIZE) {
|
|
computeBestBitWidthPlan(inValues, tmpInPos, workspace);
|
|
const bestBitWidthPlan = workspace.bestBitWidthPlan;
|
|
const bitWidth = bestBitWidthPlan[0];
|
|
const exceptionCount = bestBitWidthPlan[1];
|
|
const maxBitWidth = bestBitWidthPlan[2];
|
|
const exceptionBitWidth = exceptionCount > 0 ? maxBitWidth - bitWidth : 0;
|
|
byteContainerPos = writeBlockHeader(workspace, byteContainerPos, bitWidth, exceptionCount, maxBitWidth);
|
|
byteContainerPos = recordBlockExceptions(workspace, inValues, tmpInPos, bitWidth, exceptionCount, exceptionBitWidth, byteContainerPos);
|
|
packBlock(inValues, tmpInPos, bitWidth, state);
|
|
}
|
|
const pageEndOutPos = state.outPos;
|
|
state.inPos = tmpInPos;
|
|
state.out[headerPos] = (pageEndOutPos - headerPos) | 0;
|
|
const byteSize = byteContainerPos;
|
|
byteContainerPos = padByteContainerToInt32(workspace, byteContainerPos);
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + 1);
|
|
state.out[state.outPos++] = byteSize | 0;
|
|
writeByteContainerInts(workspace, state, byteContainerPos);
|
|
writeExceptionStreams(workspace, state);
|
|
}
|
|
function encodeAlignedPages(inValues, inLength, state, workspace) {
|
|
const alignedLength = greatestMultiple(inLength, BLOCK_SIZE);
|
|
const finalInPos = state.inPos + alignedLength;
|
|
while (state.inPos !== finalInPos) {
|
|
const thisSize = Math.min(PAGE_SIZE, finalInPos - state.inPos);
|
|
encodePage(inValues, thisSize, state, workspace);
|
|
}
|
|
}
|
|
function encode(inValues, inLength, state, workspace) {
|
|
const alignedLength = greatestMultiple(inLength, BLOCK_SIZE);
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + 1);
|
|
state.out[state.outPos++] = alignedLength;
|
|
if (alignedLength === 0)
|
|
return;
|
|
encodeAlignedPages(inValues, alignedLength, state, workspace);
|
|
}
|
|
/**
|
|
* VByte encoding for FastPFOR tail values (MSB=1 terminator).
|
|
* Note: Inverts standard Protobuf Varint (MSB=0 terminator), so we cannot reuse generic methods.
|
|
*/
|
|
function encodeVByte(inValues, inLength, state, workspace) {
|
|
if (inLength === 0)
|
|
return;
|
|
const requiredBytes = inLength * 5 + 3;
|
|
workspace.byteContainer = ensureUint8Capacity(workspace.byteContainer, requiredBytes);
|
|
const start = state.inPos;
|
|
let bytePos = 0;
|
|
for (let k = start; k < start + inLength; k++) {
|
|
let value = inValues[k] >>> 0;
|
|
while (value >= 0x80) {
|
|
workspace.byteContainer[bytePos++] = value & 0x7f;
|
|
value >>>= 7;
|
|
}
|
|
workspace.byteContainer[bytePos++] = (value | 0x80) & 0xff;
|
|
}
|
|
while ((bytePos & 3) !== 0)
|
|
workspace.byteContainer[bytePos++] = 0;
|
|
const intsToWrite = bytePos / 4;
|
|
state.out = ensureInt32Capacity(state.out, state.outPos + intsToWrite);
|
|
let outIdx = state.outPos;
|
|
for (let i = 0; i < bytePos; i += 4) {
|
|
const packedWord = workspace.byteContainer[i] |
|
|
(workspace.byteContainer[i + 1] << 8) |
|
|
(workspace.byteContainer[i + 2] << 16) |
|
|
(workspace.byteContainer[i + 3] << 24) |
|
|
0;
|
|
state.out[outIdx++] = packedWord;
|
|
}
|
|
state.outPos = outIdx;
|
|
state.inPos = (state.inPos + inLength) | 0;
|
|
}
|
|
/**
|
|
* Encodes an int32 stream using the FastPFOR wire format (pages + VByte tail).
|
|
*/
|
|
export function encodeFastPforInt32WithWorkspace(values, workspace) {
|
|
const state = { inPos: 0, outPos: 0, out: new Uint32Array(values.length + 1024) };
|
|
encode(values, values.length, state, workspace);
|
|
const remaining = values.length - state.inPos;
|
|
encodeVByte(values, remaining, state, workspace);
|
|
return state.out.subarray(0, state.outPos);
|
|
}
|
|
//# sourceMappingURL=fastPforEncoder.js.map
|