UNPKG

malloc

Version:

Simple malloc() & free() implementation on top of buffers and array buffers.

526 lines (439 loc) 15.5 kB
import {default as Allocator, verifyHeader} from "../src"; import randomNumbers from "./random.json"; const benchmark = createBenchmark(); ensureDeterministicRandom(); describe('Allocator', function () { describe('constructor()', function () { describe('Buffer', function () { let instance; let dupe; it('should create a new instance', function () { instance = new Allocator(new Buffer(1024).fill(123)); }); it('should prepare the header', function () { verifyHeader(instance.int32Array).should.equal(true); }); it('should create a new instance from an existing buffer', function () { dupe = new Allocator(instance.buffer, instance.byteOffset, instance.byteLength); verifyHeader(dupe.int32Array).should.equal(true); }); it('should allocate from both instances correctly', function () { const address1 = instance.alloc(16); const address2 = dupe.alloc(16); address2.should.be.above(address1); address2.should.equal(address1 + 16 + 8); }); }); describe('ArrayBuffer', function () { let instance; it('should create a new instance', function () { instance = new Allocator(new ArrayBuffer(1024)); }); it('should prepare the header', function () { verifyHeader(instance.int32Array).should.equal(true); }); }); describe('Bad Constructor', function () { it('should not accept undefined', function () { (() => new Allocator()).should.throw(TypeError); }); it('should not accept an array', function () { (() => new Allocator([1,2,3])).should.throw(TypeError); }); }); describe('Buffer offset', function () { it('should create a new instance with a byte offset', function () { const instance = new Allocator(new Buffer(1024), 4); }); it('should create a new instance with byte offsets and lengths', function () { const instance = new Allocator(new Buffer(4096), 4, 1024); }); }); describe('ArrayBuffer offset', function () { it('should create a new instance with a byte offset', function () { const instance = new Allocator(new ArrayBuffer(1024), 4); }); it('should create a new instance with byte offsets and lengths', function () { const instance = new Allocator(new ArrayBuffer(4096), 4, 1024); }); }); }); describe('Out of memory', function () { let instance; it('should create a new instance', function () { instance = new Allocator(new Buffer(1024).fill(123)); }); it('should allocate some space', function () { const address = instance.alloc(512); address.should.be.above(0); }); it('should exhaust the space in the instance', function () { const address = instance.alloc(512); address.should.equal(0); }); }); describe('workflow', function () { let instance = new Allocator(new Buffer(4096).fill(127)); it('should allocate some bytes', function () { const address1 = instance.alloc(64); const address2 = instance.alloc(64); const address3 = instance.alloc(64); instance.alloc(128); instance.free(address1); instance.free(address3); }); it('should allocate some bytes, free some of them, allocates some more', function () { const addresses = Array.from({length: 10}, (_, index) => instance.alloc(index % 2 ? 64 : 128)); const freed = addresses.filter((_, index) => index % 3 === 1).map(address => instance.free(address)); const addresses2 = Array.from({length: 10}, (_, index) => instance.alloc(index % 2 ? 64 : 128)); addresses2.forEach(address => instance.free(address)); }); }); describe('.calloc()', function () { let instance = new Allocator(new Buffer(4096).fill(127)); it('should allocate some bytes and clear them', function () { const address = instance.calloc(128); const uint8Array = new Uint8Array(instance.buffer); for (let i = 0; i < 128; i++) { uint8Array[address + i].should.equal(0); } }); it('should allocate and clear less than the minimum allocation size', function () { const address = instance.calloc(8); const uint8Array = new Uint8Array(instance.buffer); for (let i = 0; i < 16; i++) { uint8Array[address + i].should.equal(0); } }); it('should fail to allocate too many bytes', function () { instance.calloc(3820).should.equal(0); }); }); describe('alloc(), sizeOf() and free()', function () { let instance = new Allocator(new Buffer(4096).fill(127)); it('should alloc() less than the minimum size', function () { const address = instance.alloc(1); instance.sizeOf(address).should.equal(16); }); it('should fail to allocate more than the capacity', function () { (() => instance.alloc(4096 * 2)).should.throw(RangeError); }); it('should not free an invalid address', function () { (() => instance.free(-1)).should.throw(RangeError); }); it('should not free an address within the header', function () { (() => instance.free(16)).should.throw(RangeError); }); it('should not free an address with an invalid alignment', function () { (() => instance.free(777)).should.throw(RangeError); }); it('should not free an address larger than the array', function () { (() => instance.free(4096 * 2)).should.throw(RangeError); }); it('should not free an unallocated address', function () { (() => instance.free(1024)).should.throw(Error); }); it('should not check the size of an address within the header', function () { (() => instance.sizeOf(20)).should.throw(Error); }); it('should not check the size of a negative address', function () { (() => instance.sizeOf(20)).should.throw(Error); }); it('should not check the size of a too-large address', function () { (() => instance.sizeOf(Math.pow(2,32))).should.throw(Error); }); it('should not check the size of an invalid address', function () { (() => instance.sizeOf(777)).should.throw(Error); }); }); describe('Alloc() exhaustively', function () { let instance = new Allocator(new Buffer(4096).fill(127)); const addresses = []; it('should repeatedly allocate 16 byte chunks until it exhausts the available space', function () { let prev = 0; let next = 0; let counter = 0; while ((next = instance.alloc(16)) !== 0) { prev = next; addresses.push(next); counter++; } counter.should.equal(159); }); it('should check the size of all the addresses', function () { addresses.forEach(address => { instance.sizeOf(address).should.be.within(16, 32); }); }); it('should free all the available addresses in reverse order', function () { addresses.reverse().forEach(address => { instance.free(address).should.be.within(16, 32); }); }); }); if (!process.env.MALLOC_FAST_TESTS) { // Warning: Increasing the number of mutations has an exponential effect on test time. mutate([ 128, 64, 96, 256, 128, 72, 256 ]); } (process.env.NODE_ENV !== "production" ? describe.skip : describe)('Benchmarks', function () { let buffer = new Buffer(1024 * 1024 * 20); let instance; beforeEach(() => { buffer.fill(123); instance = new Allocator(buffer); }); afterEach(() => { instance.buffer = null; instance = null; }); after(() => { buffer = null; if (typeof gc === 'function') { gc(); } }); benchmark('allocate', 1000000, { alloc () { instance.alloc(20); } }); benchmark('allocate and free', 1000000, { alloc () { instance.free(instance.alloc(128)); } }); }); }); function d (input) { console.log(JSON.stringify(input, null, 2)); } function permutations (input: Array) { if (input.length == 0) { return [[]]; } const result = []; for (let i = 0; i < input.length; i++) { const clone = input.slice(); const start = clone.splice(i, 1); const tail = permutations(clone); for (let j = 0; j < tail.length; j++) { result.push(start.concat(tail[j])); } } return result; } function debugOnce (input) { return [input]; } function mutate (input: number[]) { //debugOnce([ 64, 72, 128, 96, 256, 128, 256]).forEach(sizes => { permutations(input).forEach(sizes => { describe(`Sizes: ${sizes.join(', ')}`, function () { describe('Sequential', function () { let instance; before(() => { instance = new Allocator(new Buffer(16000).fill(123)); }); after(() => { instance.buffer = null; instance = null; }); let addresses; it('should allocate', function () { addresses = sizes.map(item => instance.alloc(item)); }); it('should inspect the results', function () { const {blocks} = instance.inspect(); sizes.forEach((size, index) => { blocks[index].type.should.equal('used') blocks[index].offset.should.equal(addresses[index]); blocks[index].size.should.equal(size); }); }); it('should free blocks in order', function () { addresses.forEach(address => instance.free(address)); }); it('should inspect the freed blocks', function () { const {blocks} = instance.inspect(); blocks.length.should.equal(1); blocks[0].type.should.equal('free'); }); }); describe('Alloc & Free', function () { let instance; before(() => { instance = new Allocator(new Buffer(16000).fill(123)); }); after(() => { instance.buffer = null; instance = null; }); let addresses; it('should allocate', function () { addresses = sizes.map(address => instance.alloc(address)); }); it('should free & alloc again', function () { addresses = addresses.map((address, index) => { const size = sizes[(index + 1) % sizes.length]; instance.free(address); return instance.alloc(size); }); }); it('should inspect the blocks', function () { const {blocks} = instance.inspect(); }); it('should free the blocks', function () { addresses.forEach(address => instance.free(address)); }); it('should inspect the freed blocks', function () { const {blocks} = instance.inspect(); blocks.length.should.equal(1); blocks[0].type.should.equal('free'); }); }); describe('Alloc, Alloc, Free, Reverse, Alloc', function () { let instance; before(() => { instance = new Allocator(new Buffer(16000).fill(123)); }); after(() => { instance.buffer = null; instance = null; }); let addresses, extra; it('should allocate', function () { addresses = sizes.reduce((addresses, size) => { return addresses.concat(instance.alloc(size), instance.alloc(size)); }, []); addresses.every(value => value.should.be.above(0)); }); it('should free half of the allocated addresses', function () { addresses = addresses.map((address, index) => { if (index % 2 === 0) { return address; } else { instance.free(address); } }).filter(id => id); }); it('should inspect the blocks', function () { const {blocks} = instance.inspect(); blocks.forEach((block, index) => { if (index % 2 === 0) { block.type.should.equal('used'); } else { block.type.should.equal('free'); } }); }); it('should allocate', function () { extra = sizes.reduce((addresses, size) => { return addresses.concat(instance.alloc(size)); }, []); }); it('should free the blocks', function () { addresses.forEach(address => instance.free(address)); extra.forEach(address => { instance.free(address); }); }); it('should inspect the freed blocks', function () { const {blocks} = instance.inspect(); blocks.length.should.equal(1); blocks[0].type.should.equal('free'); }); }); }); }); } function createBenchmark () { function benchmark (name, limit, ...fns) { let factor = 1; if (typeof limit === 'function') { fns.unshift(limit); limit = 1000; } if (typeof fns[0] === 'number') { factor = fns.shift(); } it(`benchmark: ${name}`, benchmarkRunner(name, limit, factor, flattenBenchmarkFunctions(fns))); }; benchmark.skip = function skipBenchmark (name) { it.skip(`benchmark: ${name}`); } benchmark.only = function benchmark (name, limit, ...fns) { let factor = 1; if (typeof limit !== 'number') { fns.unshift(limit); limit = 1000; } if (typeof fns[0] === 'number') { factor = fns.shift(); } it.only(`benchmark: ${name}`, benchmarkRunner(name, limit, factor, flattenBenchmarkFunctions(fns))); }; function benchmarkRunner (name, limit, factor, fns) { return async function () { this.timeout(10000); console.log(`\tStarting benchmark: ${name}\n`); let fastest = { name: null, score: null }; let slowest = { name: null, score: null }; fns.forEach(([name,fn]) => { const start = process.hrtime(); for (let j = 0; j < limit; j++) { fn(j, limit); } let [seconds, ns] = process.hrtime(start); seconds += ns / 1000000000; const perSecond = Math.round(limit / seconds) * factor; if (fastest.score === null || fastest.score < perSecond) { fastest.name = name; fastest.score = perSecond; } if (slowest.score === null || slowest.score > perSecond) { slowest.name = name; slowest.score = perSecond; } console.log(`\t${name} benchmark done in ${seconds.toFixed(4)} seconds, ${perSecond} operations per second.`); }); if (fns.length > 1) { const diff = (fastest.score - slowest.score) / slowest.score * 100; console.log(`\n\t${fastest.name} was ${diff.toFixed(2)}% faster than ${slowest.name}`); } }; } function flattenBenchmarkFunctions (fns: Array<Object|Function>): Array { return fns.reduce((flat, item, index) => { if (typeof item === "object") { flat.push(...Object.keys(item).map(name => [name, item[name]])); } else { flat.push([item.name || "fn" + index, item]); } return flat; }, []); } return benchmark; } function ensureDeterministicRandom () { let index = 21; Math.random = function () { return randomNumbers[index++ % randomNumbers.length]; }; }