UNPKG

@jawis/shared-page-heap

Version:
362 lines (361 loc) 16.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SharedTreeHeap = void 0; const _jab_1 = require("^jab"); const _shared_algs_1 = require("^shared-algs"); const internal_1 = require("./internal"); const CHUNK_COUNT_OFFSET = 0; //number of allocated chunks. const PAGE_COUNT_OFFSET = CHUNK_COUNT_OFFSET + 1; //the length of the tree. const META_DATA_LENGTH = PAGE_COUNT_OFFSET + 1; //trouble, if not multiple of 8. const META_DATA_BYTES = Uint32Array.BYTES_PER_ELEMENT * META_DATA_LENGTH; /** * A tree is laid out sequentially in memory. * * - A left Left-compact tree. * - With a fixed left endpoint, and a dynamic right endpoint for adding/removing pages. * - Pages can't be inserted/deleted in the middle of the tree. * - Each page contains valid bits, to indicate which data chunks are currently in use. * - The user can interpret the data in any way, the only restrictions are: * - Allocate finds an invalid chunk, makes i valid, and returns it. * - Deallocated throws if given chunk isn't valid. * - Must initialize all memory itself, because this is the most basic data structure and the sharedArray could be reused. * * Complexity * - Get is contant time, and requires no memory access. * - maybe we could accept a single memory access, in order to support relocation and garbage collection. * Maybe a better solution would be a way to signal to users, that a re-allocation would be desirable. * - Allocation and deallocation is log(n) * * notes * - Maintains a tree with a full subtree on the left hand-side. * - More precisely: All subtrees reachable by at least one left edge are perfect. * - Pages are zero-indexed. * - Pages are are always added/removed at the right end. * - Edges are virtual. They can be determined from the bit-representation of page-index. * - Similar implementation/purpose as SharedChunkHeap * - maybe that implementation is better, because the linked list is constant time in (de)allocation. * The only difference is, that this implementation must keep a free list instead of deallocation * in the parent heap. * * memory layout * - meta data * - pages (sequentially in memory) * * page layout * 2 uint32 free-bit-boolean. If left/right subtree has any valid bits it's 1, otherwise 0. * 64 bits valid-bits * n bytes space for chunks. For external use. * * structure of reference (heighest bits first) * 27 bits page index * 1 bit chunk index * 4 bits chunk integrity version * * todo * - What is the purpose of having chunks? Maybe so the validity bytes can be fully used. They have to be 8 bytes. * So alignment isn't problem. It would also be better to have free-bit-booleans co-located in memory rows, so the * the search for pages makes the most use for memory fetches. * - pagecount isn't decreased. But is the heighest unused pages could be removed, if user's want to reclaim place. * - Shrink pagecount isn't implemented. Could remove pages from right, when they becomes empty. */ class SharedTreeHeap { /** * */ constructor(deps) { this.deps = deps; /** * */ this.pack = () => ({ maxSize: this.deps.maxSize, dataSize: this.deps.dataSize, sharedArray: this.deps.sharedArray, useChunkVersion: this.deps.useChunkVersion, initialized: true, }); /** * */ this.getChuckByteOffset = (ref) => { const { pageIndex, chunkIndex } = this.decode(ref); const pageByteOffset = META_DATA_BYTES + pageIndex * this.pageByteSize + SharedTreeHeap.PAGE_HEADER_BYTE_SIZE; // prettier-ignore return pageByteOffset + chunkIndex * this.deps.dataSize; }; /** * Get previously allocated memory. * * - This is used in other threads, to get the shared memory, dynamically. */ this.get = (ref, TypedArray) => { (0, _jab_1.assert)(Number.isInteger(this.deps.dataSize / TypedArray.BYTES_PER_ELEMENT)); return (0, _jab_1.makeTypedArray)(this.deps.sharedArray, TypedArray, this.getChuckByteOffset(ref), this.deps.dataSize / TypedArray.BYTES_PER_ELEMENT); }; /** * Allocate the given size of shared memory. * * - The memory isn't locked, so it can be retrieved in other threads. * - The memory is only protected against reuse for other allocations. * - All threads with the returned reference can get/deallocate the memory. * */ this.allocate = (TypedArray, zeroFill = true) => { const ref = this.allocate2(); const array = this.get(ref, TypedArray); if (array instanceof BigInt64Array || array instanceof BigUint64Array) { zeroFill && array.fill(BigInt(0)); } else { zeroFill && array.fill(0); } return { ref, array }; }; /** * udgår */ this.allocate2 = () => { const index = this.tryAllocate(); if (index === undefined) { throw new Error("Not enough space, max: " + this.deps.maxSize); // prettier-ignore } return index; }; /** * * - Increases the data structure if it's needed. And if the allotted space allows it. * * impl * - The left subpage of the added page will always be full. Either it's a leaf page. Or it's * a inner page and the left sub page is filled, because we only add pages, when we run out of space. * - Note: The inserted page is the right most in the tree, because it's the largest page. Hence it's reachable * via only right edges. Otherwise a ancestor would be to the right it. * - The pages that will have free-bits below them after insertion, are the pages from the root to the * inserted page. I.e. the pages reachable from the root by following right edges. * - Note the inserted page shouldn't have a free-bits set true for right subtree, but the `_tryGet` will have same * semantics. It will just try to recurse and set free-bits to false. */ this.tryAllocate = () => { const index = this._tryAllocate((0, internal_1._getRoot)(this.deps.sharedArray[PAGE_COUNT_OFFSET])); // prettier-ignore if (index !== undefined) { return index; } //it's was out of space if (this.deps.sharedArray[PAGE_COUNT_OFFSET] < this.maxPages) { // we can increase pages. this.deps.sharedArray[PAGE_COUNT_OFFSET]++; const cachedPageCount = this.deps.sharedArray[PAGE_COUNT_OFFSET]; //set free-bit-boolean from root-page (possibly new) to all transitive right pages. let pageIndex = (0, internal_1._getRoot)(cachedPageCount); do { const freeBitArray = this.getFreeBitArray(pageIndex); freeBitArray[1] = 1; } while ((pageIndex = (0, internal_1._rightNode)(pageIndex, cachedPageCount))); // now there should be something const newIndex = this._tryAllocate((0, internal_1._getRoot)(cachedPageCount)); (0, _jab_1.assert)(newIndex !== undefined, "Impossible"); return newIndex; } //out of space, and can't expand. }; /** * in-order dfs traversal. * - Finds the left-most page with free space. */ this._tryAllocate = (pageIndex) => { if (pageIndex + 1 > this.deps.sharedArray[PAGE_COUNT_OFFSET]) { throw new Error("Impossible"); } const freeBitArray = this.getFreeBitArray(pageIndex); //left pages if (freeBitArray[0] === 1) { const left = (0, internal_1._leftNode)(pageIndex); if (left !== undefined) { const index = this._tryAllocate(left); if (index !== undefined) { return index; } } //left sub tree is filled now. freeBitArray[0] = 0; //continue to search in own and right subpages. } // own page const chunk = this.getValidityVector(pageIndex).tryGet(); if (chunk !== undefined) { const { index, ref } = this.encode(pageIndex, chunk.index, chunk.version); if (index >= this.deps.maxSize) { //index was found in this page, but the size limit is reach within this exact page. return; } else { this.deps.sharedArray[CHUNK_COUNT_OFFSET]++; //found in own page return ref; } } //right pages if (freeBitArray[1] === 1) { const right = (0, internal_1._rightNode)(pageIndex, this.deps.sharedArray[PAGE_COUNT_OFFSET]); if (right !== undefined) { return this._tryAllocate(right); } //right sub tree is filled now. freeBitArray[1] = 0; } //nothing found return; }; /** * */ this.deallocate = (ref) => { const { pageIndex, chunkIndex, version } = this.decode(ref); this.deps.sharedArray[CHUNK_COUNT_OFFSET]--; this._deallocate(ref, pageIndex, chunkIndex, version, (0, internal_1._getRoot)(this.deps.sharedArray[PAGE_COUNT_OFFSET])); }; /** * in-order dfs traversal. * - Finds the page and invalidates the chunk. (it could be done in constant time) * - Sets free-bit-booleans. (which require a log(n) traversal) * * impl * - sets the page 'statistics' on the way up. Because there are user input, a throw * could corrupt the tree. * - Might actually not be a semantic problem, because `tryGet` will just perform the search, * and then set the page 'statistics' correctly again. * * todo * - extract, so this only does searchPage. */ this._deallocate = (ref, pageIndex, chunkIndex, version, curPageIndex) => { if (curPageIndex === undefined) { throw new Error("Reference out of range: " + ref); } const freeBitArray = this.getFreeBitArray(curPageIndex); //left if (pageIndex < curPageIndex) { this._deallocate(ref, pageIndex, chunkIndex, version, (0, internal_1._leftNode)(curPageIndex)); freeBitArray[0] = 1; return; } //own if (pageIndex < curPageIndex + 1) { this.getValidityVector(curPageIndex).invalidate(chunkIndex, version); return; } //right this._deallocate(ref, pageIndex, chunkIndex, version, (0, internal_1._rightNode)(curPageIndex, this.deps.sharedArray[PAGE_COUNT_OFFSET])); freeBitArray[1] = 1; }; /** * SharedChunkHeap.encode seems better. And can we extract to ValidityVector? */ this.encode = (pageIndex, chunkIndex, _version) => { if (chunkIndex > 1) { throw new Error("not impl"); } const version = this.deps.useChunkVersion ? _version : 0; if (version > 15) { throw new Error("Version too high"); } const baseIndex = pageIndex * SharedTreeHeap.DATA_COUNT_PER_PAGE; const index = baseIndex + chunkIndex; return { index, ref: version + (index << 4) }; }; /** * SharedChunkHeap.decode seems better. And can we extract to ValidityVector? */ this.decode = (ref) => { const version = this.deps.useChunkVersion ? ref & 0xf : 1; (0, _jab_1.assert)(version !== 0, "Reference isn't valid: ", { ref, version }); const index = ref >>> 4; const pageIndex = Math.floor(index / SharedTreeHeap.DATA_COUNT_PER_PAGE); // prettier-ignore if (pageIndex < 0 || pageIndex >= this.deps.sharedArray[PAGE_COUNT_OFFSET]) { (0, _jab_1.err)("Reference isn't valid: ", { ref, pageIndex }); } const baseIndex = pageIndex * SharedTreeHeap.DATA_COUNT_PER_PAGE; const chunkIndex = index - baseIndex; const bitVector = this.getValidityVector(pageIndex); (0, _jab_1.assert)(bitVector.isValid(chunkIndex, version), "Reference isn't valid: ", { ref, index, pageIndex, chunkIndex, version, }); return { pageIndex, chunkIndex, version }; }; /** * */ this.getFreeBitArray = (pageIndex) => { const freeBitOffset = META_DATA_BYTES / 4 + (pageIndex * this.pageByteSize) / 4; const freeBitArray = this.deps.sharedArray.subarray(freeBitOffset, freeBitOffset + SharedTreeHeap.FREE_BIT_BYTE_SIZE / 4); // prettier-ignore (0, _jab_1.assert)(freeBitArray.length === 2, undefined, { freeBitArray, pageIndex, sharedArray: this.deps.sharedArray, freeBitOffset, }); // prettier-ignore return freeBitArray; }; /** * */ this.getValidityVector = (pageIndex) => { const freeBitOffset = META_DATA_BYTES / 4 + (pageIndex * this.pageByteSize) / 4; // prettier-ignore const validityVectorOffset = freeBitOffset + SharedTreeHeap.VALIDITY_VECTOR_BYTE_OFFSET / 4; // prettier-ignore const sharedArray = this.deps.sharedArray.subarray(validityVectorOffset, validityVectorOffset + SharedTreeHeap.VALIDITY_VECTOR_BYTE_SIZE / 4); // prettier-ignore (0, _jab_1.assert)(sharedArray.length === 2, undefined, { sharedArray }); return new _shared_algs_1.SharedValidityVector({ size: SharedTreeHeap.VALIDITY_VECTOR_BYTE_SIZE / 4, sharedArray, useChunkVersion: this.deps.useChunkVersion, }); }; /** * */ this.toString = () => (0, _jab_1.tos)(this.deps.sharedArray); (0, _jab_1.assert)(deps.dataSize > 0, "Datasize must be positive: " + deps.dataSize); // prettier-ignore (0, _jab_1.assert)(this.deps.maxSize > 0, "Max size must be positive: " + this.deps.maxSize); // prettier-ignore (0, _jab_1.assert)(Number.isInteger(this.deps.dataSize / 4), "data size must be multiple of 4, was: " + this.deps.dataSize); // prettier-ignore //check byte size const byteSize = SharedTreeHeap.getExpectedByteSize(this.deps.maxSize, this.deps.dataSize); // prettier-ignore (0, _jab_1.assertEq)(this.deps.sharedArray.byteLength, byteSize, "The given sharedArray has wrong size."); // prettier-ignore //derived this.maxPages = Math.ceil(this.deps.maxSize / SharedTreeHeap.DATA_COUNT_PER_PAGE); // prettier-ignore this.pageByteSize = SharedTreeHeap.PAGE_HEADER_BYTE_SIZE + SharedTreeHeap.DATA_COUNT_PER_PAGE * this.deps.dataSize; //state if (!deps.initialized) { this.deps.sharedArray[CHUNK_COUNT_OFFSET] = 0; this.deps.sharedArray[PAGE_COUNT_OFFSET] = 1; } } /** * */ get dataSize() { return this.deps.dataSize; } /** * */ get count() { return this.deps.sharedArray[CHUNK_COUNT_OFFSET]; } } exports.SharedTreeHeap = SharedTreeHeap; SharedTreeHeap.FREE_BIT_BYTE_SIZE = 8; SharedTreeHeap.VALIDITY_VECTOR_BYTE_SIZE = 8; SharedTreeHeap.VALIDITY_VECTOR_BYTE_OFFSET = 8; SharedTreeHeap.PAGE_HEADER_BYTE_SIZE = SharedTreeHeap.FREE_BIT_BYTE_SIZE + SharedTreeHeap.VALIDITY_VECTOR_BYTE_SIZE; // prettier-ignore SharedTreeHeap.DATA_OVERHEAD = 8; SharedTreeHeap.DATA_COUNT_PER_PAGE = 2; /** * */ SharedTreeHeap.getExpectedByteSize = (n, dataSize) => { const headerCount = Math.ceil((n * SharedTreeHeap.DATA_OVERHEAD) / SharedTreeHeap.PAGE_HEADER_BYTE_SIZE); const headerBytes = headerCount * SharedTreeHeap.PAGE_HEADER_BYTE_SIZE; return META_DATA_BYTES + headerBytes + n * dataSize; };