@thi.ng/malloc
Version:
ArrayBuffer based malloc() impl for hybrid JS/WASM use cases, based on thi.ng/tinyalloc
415 lines (414 loc) • 11 kB
JavaScript
import {
SIZEOF,
typedArray
} from "@thi.ng/api/typedarray";
import { align } from "@thi.ng/binary/align";
import { isNumber } from "@thi.ng/checks/is-number";
import { assert } from "@thi.ng/errors/assert";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
const STATE_FREE = 0;
const STATE_USED = 1;
const STATE_TOP = 2;
const STATE_END = 3;
const STATE_ALIGN = 4;
const STATE_FLAGS = 5;
const STATE_MIN_SPLIT = 6;
const MASK_COMPACT = 1;
const MASK_SPLIT = 2;
const SIZEOF_STATE = 7 * 4;
const MEM_BLOCK_SIZE = 0;
const MEM_BLOCK_NEXT = 1;
const SIZEOF_MEM_BLOCK = 2 * 4;
class MemPool {
buf;
start;
u8;
u32;
state;
constructor(opts = {}) {
this.buf = opts.buf ? opts.buf : new ArrayBuffer(opts.size || 4096);
this.start = opts.start != null ? align(Math.max(opts.start, 0), 4) : 0;
this.u8 = new Uint8Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.state = new Uint32Array(this.buf, this.start, SIZEOF_STATE / 4);
if (!opts.skipInitialization) {
const _align = opts.align || 8;
assert(
_align >= 8,
`invalid alignment: ${_align}, must be a pow2 and >= 8`
);
const top = this.initialTop(_align);
const resolvedEnd = opts.end != null ? Math.min(opts.end, this.buf.byteLength) : this.buf.byteLength;
if (top >= resolvedEnd) {
illegalArgs(
`insufficient address range (0x${this.start.toString(
16
)} - 0x${resolvedEnd.toString(16)})`
);
}
this.align = _align;
this.doCompact = opts.compact !== false;
this.doSplit = opts.split !== false;
this.minSplit = opts.minSplit || 16;
this.end = resolvedEnd;
this.top = top;
this._free = 0;
this._used = 0;
}
}
stats() {
const listStats = (block) => {
let count = 0;
let size = 0;
while (block) {
count++;
size += this.blockSize(block);
block = this.blockNext(block);
}
return { count, size };
};
const free = listStats(this._free);
return {
free,
used: listStats(this._used),
top: this.top,
available: this.end - this.top + free.size,
total: this.buf.byteLength
};
}
callocAs(type, num, fill = 0) {
const block = this.mallocAs(type, num);
block?.fill(fill);
return block;
}
mallocAs(type, num) {
const addr = this.malloc(num * SIZEOF[type]);
return addr ? typedArray(type, this.buf, addr, num) : void 0;
}
calloc(bytes, fill = 0) {
const addr = this.malloc(bytes);
addr && this.u8.fill(fill, addr, addr + bytes);
return addr;
}
malloc(bytes) {
if (bytes <= 0) {
return 0;
}
const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
const end = this.end;
let top = this.top;
let block = this._free;
let prev = 0;
while (block) {
const blockSize = this.blockSize(block);
const isTop = block + blockSize >= top;
if (isTop || blockSize >= paddedSize) {
return this.mallocTop(
block,
prev,
blockSize,
paddedSize,
isTop
);
}
prev = block;
block = this.blockNext(block);
}
block = top;
top = block + paddedSize;
if (top <= end) {
this.initBlock(block, paddedSize, this._used);
this._used = block;
this.top = top;
return __blockDataAddress(block);
}
return 0;
}
mallocTop(block, prev, blockSize, paddedSize, isTop) {
if (isTop && block + paddedSize > this.end) return 0;
if (prev) {
this.unlinkBlock(prev, block);
} else {
this._free = this.blockNext(block);
}
this.setBlockNext(block, this._used);
this._used = block;
if (isTop) {
this.top = block + this.setBlockSize(block, paddedSize);
} else if (this.doSplit) {
const excess = blockSize - paddedSize;
excess >= this.minSplit && this.splitBlock(block, paddedSize, excess);
}
return __blockDataAddress(block);
}
realloc(ptr, bytes) {
if (bytes <= 0) {
return 0;
}
const oldAddr = __blockSelfAddress(ptr);
let newAddr = 0;
let block = this._used;
let blockEnd = 0;
while (block) {
if (block === oldAddr) {
[newAddr, blockEnd] = this.reallocBlock(block, bytes);
break;
}
block = this.blockNext(block);
}
if (newAddr && newAddr !== oldAddr) {
this.u8.copyWithin(
__blockDataAddress(newAddr),
__blockDataAddress(oldAddr),
blockEnd
);
}
return __blockDataAddress(newAddr);
}
reallocBlock(block, bytes) {
const blockSize = this.blockSize(block);
const blockEnd = block + blockSize;
const isTop = blockEnd >= this.top;
const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
if (paddedSize <= blockSize) {
if (this.doSplit) {
const excess = blockSize - paddedSize;
if (excess >= this.minSplit) {
this.splitBlock(block, paddedSize, excess);
} else if (isTop) {
this.top = block + paddedSize;
}
} else if (isTop) {
this.top = block + paddedSize;
}
return [block, blockEnd];
}
if (isTop && block + paddedSize < this.end) {
this.top = block + this.setBlockSize(block, paddedSize);
return [block, blockEnd];
}
this.free(block);
return [__blockSelfAddress(this.malloc(bytes)), blockEnd];
}
reallocArray(array, num) {
if (array.buffer !== this.buf) {
return;
}
const addr = this.realloc(
array.byteOffset,
num * array.BYTES_PER_ELEMENT
);
return addr ? new array.constructor(this.buf, addr, num) : void 0;
}
free(ptrOrArray) {
let addr;
if (!isNumber(ptrOrArray)) {
if (ptrOrArray.buffer !== this.buf) {
return false;
}
addr = ptrOrArray.byteOffset;
} else {
addr = ptrOrArray;
}
addr = __blockSelfAddress(addr);
let block = this._used;
let prev = 0;
while (block) {
if (block === addr) {
if (prev) {
this.unlinkBlock(prev, block);
} else {
this._used = this.blockNext(block);
}
this.insert(block);
this.doCompact && this.compact();
return true;
}
prev = block;
block = this.blockNext(block);
}
return false;
}
freeAll() {
this._free = 0;
this._used = 0;
this.top = this.initialTop();
}
release() {
delete this.u8;
delete this.u32;
delete this.state;
delete this.buf;
return true;
}
get align() {
return this.state[STATE_ALIGN];
}
set align(x) {
this.state[STATE_ALIGN] = x;
}
get end() {
return this.state[STATE_END];
}
set end(x) {
this.state[STATE_END] = x;
}
get top() {
return this.state[STATE_TOP];
}
set top(x) {
this.state[STATE_TOP] = x;
}
get _free() {
return this.state[STATE_FREE];
}
set _free(block) {
this.state[STATE_FREE] = block;
}
get _used() {
return this.state[STATE_USED];
}
set _used(block) {
this.state[STATE_USED] = block;
}
get doCompact() {
return !!(this.state[STATE_FLAGS] & MASK_COMPACT);
}
set doCompact(flag) {
flag ? this.state[STATE_FLAGS] |= 1 << MASK_COMPACT - 1 : this.state[STATE_FLAGS] &= ~MASK_COMPACT;
}
get doSplit() {
return !!(this.state[STATE_FLAGS] & MASK_SPLIT);
}
set doSplit(flag) {
flag ? this.state[STATE_FLAGS] |= 1 << MASK_SPLIT - 1 : this.state[STATE_FLAGS] &= ~MASK_SPLIT;
}
get minSplit() {
return this.state[STATE_MIN_SPLIT];
}
set minSplit(x) {
assert(
x > SIZEOF_MEM_BLOCK,
`illegal min split threshold: ${x}, require at least ${SIZEOF_MEM_BLOCK + 1}`
);
this.state[STATE_MIN_SPLIT] = x;
}
blockSize(block) {
return this.u32[(block >> 2) + MEM_BLOCK_SIZE];
}
/**
* Sets & returns given block size.
*
* @param block -
* @param size -
*/
setBlockSize(block, size) {
this.u32[(block >> 2) + MEM_BLOCK_SIZE] = size;
return size;
}
blockNext(block) {
return this.u32[(block >> 2) + MEM_BLOCK_NEXT];
}
/**
* Sets block next pointer to `next`. Use zero to indicate list end.
*
* @param block -
*/
setBlockNext(block, next) {
this.u32[(block >> 2) + MEM_BLOCK_NEXT] = next;
}
/**
* Initializes block header with given `size` and `next` pointer. Returns `block`.
*
* @param block -
* @param size -
* @param next -
*/
initBlock(block, size, next) {
const idx = block >>> 2;
this.u32[idx + MEM_BLOCK_SIZE] = size;
this.u32[idx + MEM_BLOCK_NEXT] = next;
return block;
}
unlinkBlock(prev, block) {
this.setBlockNext(prev, this.blockNext(block));
}
splitBlock(block, blockSize, excess) {
this.insert(
this.initBlock(
block + this.setBlockSize(block, blockSize),
excess,
0
)
);
this.doCompact && this.compact();
}
initialTop(_align = this.align) {
return align(this.start + SIZEOF_STATE + SIZEOF_MEM_BLOCK, _align) - SIZEOF_MEM_BLOCK;
}
/**
* Traverses free list and attempts to recursively merge blocks
* occupying consecutive memory regions. Returns true if any blocks
* have been merged. Only called if `compact` option is enabled.
*/
compact() {
let block = this._free;
let prev = 0;
let scan = 0;
let scanPrev;
let res = false;
while (block) {
scanPrev = block;
scan = this.blockNext(block);
while (scan && scanPrev + this.blockSize(scanPrev) === scan) {
scanPrev = scan;
scan = this.blockNext(scan);
}
if (scanPrev !== block) {
const newSize = scanPrev - block + this.blockSize(scanPrev);
this.setBlockSize(block, newSize);
const next = this.blockNext(scanPrev);
let tmp = this.blockNext(block);
while (tmp && tmp !== next) {
const tn = this.blockNext(tmp);
this.setBlockNext(tmp, 0);
tmp = tn;
}
this.setBlockNext(block, next);
res = true;
}
if (block + this.blockSize(block) >= this.top) {
this.top = block;
prev ? this.unlinkBlock(prev, block) : this._free = this.blockNext(block);
}
prev = block;
block = this.blockNext(block);
}
return res;
}
/**
* Inserts given block into list of free blocks, sorted by address.
*
* @param block -
*/
insert(block) {
let ptr = this._free;
let prev = 0;
while (ptr) {
if (block <= ptr) break;
prev = ptr;
ptr = this.blockNext(ptr);
}
if (prev) {
this.setBlockNext(prev, block);
} else {
this._free = block;
}
this.setBlockNext(block, ptr);
}
}
const __blockDataAddress = (blockAddress) => blockAddress > 0 ? blockAddress + SIZEOF_MEM_BLOCK : 0;
const __blockSelfAddress = (dataAddress) => dataAddress > 0 ? dataAddress - SIZEOF_MEM_BLOCK : 0;
export {
MemPool
};