diff --git a/README.md b/README.md index 9f1ea02c..31956779 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A few early examples to get an idea: A PSON decoder implemented in AssemblyScript. * **[TLSF memory allocator](./examples/tlsf)**
- A port of TLSF to AssemblyScript. + An implementation of the TLSF memory allocator in AssemblyScript. * **[μgc garbage collector](./examples/ugc)**
A port of μgc to AssemblyScript. diff --git a/bin/asc b/bin/asc old mode 100644 new mode 100755 diff --git a/bin/asc.js b/bin/asc.js index 48bdb8ce..d3383d98 100644 --- a/bin/asc.js +++ b/bin/asc.js @@ -1,7 +1,9 @@ -var fs = require("fs"); -var path = require("path"); -var minimist = require("minimist"); -var glob = require("glob"); +#!/usr/bin/env node +const fs = require("fs"); +const path = require("path"); +const minimist = require("minimist"); +const glob = require("glob"); +const { SourceMapConsumer, SourceMapGenerator } = require("source-map"); var assemblyscript; var isDev = true; @@ -15,8 +17,8 @@ try { assemblyscript = require("../src"); } -var conf = require("./asc.json"); -var opts = {}; +const conf = require("./asc.json"); +const opts = {}; Object.keys(conf).forEach(key => { var opt = conf[key]; @@ -30,9 +32,10 @@ Object.keys(conf).forEach(key => { (opts.boolean || (opts.boolean = [])).push(key); }); -var args = minimist(process.argv.slice(2), opts); +const args = minimist(process.argv.slice(2), opts); +const indent = 24; + var version = require("../package.json").version; -var indent = 24; if (isDev) version += "-dev"; if (args.version) { @@ -43,7 +46,7 @@ if (args.version) { } if (args.help || args._.length < 1) { - var options = []; + const options = []; Object.keys(conf).forEach(name => { var option = conf[name]; var text = " "; @@ -79,6 +82,7 @@ var parser = null; var readTime = 0; var readCount = 0; var writeTime = 0; +var writeCount = 0; var parseTime = 0; var compileTime = 0; var validateTime = 0; @@ -122,7 +126,7 @@ libDirs.forEach(libDir => { var nextText = fs.readFileSync(path.join(libDir, file), { encoding: "utf8" }); ++readCount; var time = measure(() => { - parser = assemblyscript.parseFile(nextText, "std:" + file, parser, false); + parser = assemblyscript.parseFile(nextText, ".std/" + file, parser, false); }); parseTime += time; notIoTime += time; @@ -139,12 +143,14 @@ args._.forEach(filename => { try { readTime += measure(() => { entryText = fs.readFileSync(entryPath + ".ts", { encoding: "utf8" }); + entryPath += ".ts"; }); ++readCount; } catch (e) { try { readTime += measure(() => { entryText = fs.readFileSync(entryPath + "/index.ts", { encoding: "utf8" }); + entryPath += "/index.ts"; }); ++readCount; entryPath = entryPath + "/index"; @@ -154,7 +160,7 @@ args._.forEach(filename => { } } - var nextPath; + var nextFile; var nextText; // Load entry text @@ -162,13 +168,14 @@ args._.forEach(filename => { parser = assemblyscript.parseFile(entryText, entryPath, parser, true); }); - while ((nextPath = parser.nextFile()) != null) { + while ((nextFile = parser.nextFile()) != null) { var found = false; - if (nextPath.startsWith("std:")) { + if (nextFile.startsWith(".std/")) { for (var i = 0; i < libDirs.length; ++i) { readTime += measure(() => { try { - nextText = fs.readFileSync(libDirs[i] + "/" + nextPath.substring(4) + ".ts", { encoding: "utf8" }); + nextText = fs.readFileSync(path.join(libDirs[i], nextFile.substring(4) + ".ts"), { encoding: "utf8" }); + nextFile = nextFile + ".ts"; found = true; } catch (e) {} }); @@ -179,7 +186,8 @@ args._.forEach(filename => { } else { readTime += measure(() => { try { - nextText = fs.readFileSync(nextPath + "/index.ts", { encoding: "utf8" }); + nextText = fs.readFileSync(nextFile + ".ts", { encoding: "utf8" }); + nextFile = nextFile + ".ts"; found = true; } catch (e) {} }); @@ -187,7 +195,8 @@ args._.forEach(filename => { if (!found) { readTime += measure(() => { try { - nextText = fs.readFileSync(nextPath + ".ts", { encoding: "utf8" }); + nextText = fs.readFileSync(nextFile + "/index.ts", { encoding: "utf8" }); + nextFile = nextFile + "/index.ts"; found = true; } catch (e) {} }); @@ -195,11 +204,11 @@ args._.forEach(filename => { } } if (!found) { - console.error("Imported file '" + nextPath + ".ts' not found."); + console.error("Imported file '" + nextFile + ".ts' not found."); process.exit(1); } parseTime += measure(() => { - assemblyscript.parseFile(nextText, nextPath, parser); + assemblyscript.parseFile(nextText, nextFile, parser); }); } checkDiagnostics(parser); @@ -210,6 +219,7 @@ assemblyscript.setTarget(options, 0); assemblyscript.setNoTreeShaking(options, args.noTreeShaking); assemblyscript.setNoAssert(options, args.noAssert); assemblyscript.setNoMemory(options, args.noMemory); +assemblyscript.setSourceMap(options, args.sourceMap != null); var module; compileTime += measure(() => { @@ -221,6 +231,7 @@ if (args.validate) validateTime += measure(() => { if (!module.validate()) { module.dispose(); + console.error("Validation failed"); process.exit(1); } }); @@ -234,7 +245,7 @@ else if (args.trapMode === "js") module.runPasses([ "trap-mode-js" ]); }); else if (args.trapMode !== "allow") { - console.log("Unsupported trap mode: " + args.trapMode); + console.error("Unsupported trap mode: " + args.trapMode); process.exit(1); } @@ -298,6 +309,40 @@ if (runPasses.length) module.runPasses(runPasses.map(pass => pass.trim())); }); +function processSourceMap(sourceMap, sourceMapURL) { + var json = JSON.parse(sourceMap); + return SourceMapConsumer.with(sourceMap, sourceMapURL, consumer => { + var generator = SourceMapGenerator.fromSourceMap(consumer); + json.sources.forEach(name => { + var text, found = false; + if (name.startsWith(".std/")) { + for (var i = 0, k = libDirs.length; i < k; ++i) { + readTime += measure(() => { + try { + text = fs.readFileSync(path.join(libDirs[i], name.substring(4)), { encoding: "utf8" }); + found = true; + } catch (e) {} + }); + ++readCount; + } + } else { + readTime += measure(() => { + try { + text = fs.readFileSync(name, { encoding: "utf8" }); + found = true; + } catch (e) {} + }); + ++readCount; + } + if (found) + generator.setSourceContent(name, text); + else + console.error("No source content found for file '" + name + "'."); + }); + return generator.toString(); + }); +} + if (!args.noEmit) { var hasOutput = false; @@ -310,47 +355,69 @@ if (!args.noEmit) { args.binaryFile = args.outFile; } if (args.binaryFile != null && args.binaryFile.length) { + var sourceMapURL = args.sourceMap != null + ? args.sourceMap.length + ? args.sourceMap + : path.basename(args.binaryFile) + ".map" + : null; + var binary; writeTime += measure(() => { - fs.writeFileSync(args.binaryFile, module.toBinary()); + binary = module.toBinary(sourceMapURL); // FIXME: 'not a valid URL' in FF + fs.writeFileSync(args.binaryFile, binary.output); }); + ++writeCount; + if (binary.sourceMap != null) + processSourceMap(binary.sourceMap).then(sourceMap => { + writeTime += measure(() => { + fs.writeFileSync(path.join(path.dirname(args.binaryFile), path.basename(sourceMapURL)), sourceMap, { encoding: "utf8" }); + }, err => { + throw err; + }); + ++writeCount; + }); hasOutput = true; } if (args.textFile != null && args.textFile.length) { writeTime += measure(() => { fs.writeFileSync(args.textFile, module.toText(), { encoding: "utf8" }); }); + ++writeCount; hasOutput = true; } if (args.asmjsFile != null && args.asmjsFile.length) { writeTime += measure(() => { fs.writeFileSync(args.asmjsFile, module.toAsmjs(), { encoding: "utf8" }); }); + ++writeCount; hasOutput = true; } if (!hasOutput) { - if (args.binaryFile === "") + if (args.binaryFile === "") { writeTime += measure(() => { process.stdout.write(Buffer.from(module.toBinary())); }); - else if (args.asmjsFile === "") + ++writeCount; + } else if (args.asmjsFile === "") { writeTime += measure(() => { module.printAsmjs(); }); - else + ++writeCount; + } else { writeTime += measure(() => { module.print(); }); + ++writeCount; + } } } module.dispose(); -if (args.measure) - console.error([ - "I/O Read : " + (readTime ? (readTime / 1e6).toFixed(3) + " ms (" + readCount + " files)" : "N/A"), - "I/O Write : " + (writeTime ? (writeTime / 1e6).toFixed(3) + " ms" : "N/A"), - "Parse : " + (parseTime ? (parseTime / 1e6).toFixed(3) + " ms" : "N/A"), - "Compile : " + (compileTime ? (compileTime / 1e6).toFixed(3) + " ms" : "N/A"), - "Validate : " + (validateTime ? (validateTime / 1e6).toFixed(3) + " ms" : "N/A"), - "Optimize : " + (optimizeTime ? (optimizeTime / 1e6).toFixed(3) + " ms" : "N/A") - ].join("\n")); +if (args.measure) process.on("beforeExit", () => console.error([ + "I/O Read : " + (readTime ? (readTime / 1e6).toFixed(3) + " ms (" + readCount + " files)" : "N/A"), + "I/O Write : " + (writeTime ? (writeTime / 1e6).toFixed(3) + " ms (" + writeCount + " files)" : "N/A"), + "Parse : " + (parseTime ? (parseTime / 1e6).toFixed(3) + " ms" : "N/A"), + "Compile : " + (compileTime ? (compileTime / 1e6).toFixed(3) + " ms" : "N/A"), + "Validate : " + (validateTime ? (validateTime / 1e6).toFixed(3) + " ms" : "N/A"), + "Optimize : " + (optimizeTime ? (optimizeTime / 1e6).toFixed(3) + " ms" : "N/A") +].join("\n"))); diff --git a/bin/asc.json b/bin/asc.json index 2e9cd0e6..83a30284 100644 --- a/bin/asc.json +++ b/bin/asc.json @@ -30,7 +30,7 @@ "type": "number" }, "shrinkLevel": { - "desc": "How much to focus on shrinking code size. [0-2]", + "desc": "How much to focus on shrinking code size. [0-2, s=1, z=2]", "type": "number" }, "validate": { @@ -58,6 +58,13 @@ "type": "string", "aliases": [ "a" ] }, + "sourceMap": { + "desc": [ + "Enables source map generation. Optionally takes the URL", + "used to reference the source map from the binary file." + ], + "type": "string" + }, "noTreeShaking": { "desc": "Disables compiler-level tree-shaking.", "type": "boolean" diff --git a/examples/tlsf/README.md b/examples/tlsf/README.md index 2ff61a18..f06d8be4 100644 --- a/examples/tlsf/README.md +++ b/examples/tlsf/README.md @@ -1,7 +1,8 @@ -TLSF memory allocator -===================== +![](https://s.gravatar.com/avatar/f105de3decfafc734b8eabe9a960b25d?size=48) TLSF +================= -A port of [Matt Conte's implementation](https://github.com/mattconte/tlsf) of the [TLSF](http://www.gii.upv.es/tlsf/) memory allocator to AssemblyScript. +An implementation of the [Two Level Segregate Fit](http://www.gii.upv.es/tlsf/main/docs) +memory allocator in AssemblyScript. Instructions ------------ diff --git a/examples/tlsf/assembly/LICENSE b/examples/tlsf/assembly/LICENSE deleted file mode 100644 index a1544a95..00000000 --- a/examples/tlsf/assembly/LICENSE +++ /dev/null @@ -1,36 +0,0 @@ -tlsf.ts is based on https://github.com/mattconte/tlsf - -Two Level Segregated Fit memory allocator, version 3.1. -Written by Matthew Conte - http://tlsf.baisoku.org - -Based on the original documentation by Miguel Masmano: - http://www.gii.upv.es/tlsf/main/docs - -This implementation was written to the specification -of the document, therefore no GPL restrictions apply. - -Copyright (c) 2006-2016, Matthew Conte -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -* Neither the name of the copyright holder nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MATTHEW CONTE BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/tlsf/assembly/tlsf.ts b/examples/tlsf/assembly/tlsf.ts index e7f3ef0a..b2b0613d 100644 --- a/examples/tlsf/assembly/tlsf.ts +++ b/examples/tlsf/assembly/tlsf.ts @@ -1,754 +1,457 @@ ////////////// TLSF (Two-Level Segregate Fit) Memory Allocator //////////////// -// based on https://github.com/mattconte/tlsf - BSD (see LICENSE file) // -/////////////////////////////////////////////////////////////////////////////// -// Configuration +// ╒══════════════ Block size interpretation (32-bit) ═════════════╕ +// 3 2 1 +// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 bits +// ├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┼─┴─┴─┴─┴─╫─┴─┴─┤ +// │ | FL │ SB = SL + AL │ ◄─ usize +// └───────────────────────────────────────────────┴─────────╨─────┘ +// FL: first level, SL: second level, AL: alignment, SB: small block -const SL_INDEX_COUNT_LOG2: u32 = 5; +const AL_BITS: u32 = sizeof() == sizeof() ? 2 : 3; +const AL_SIZE: usize = 1 << AL_BITS; +const AL_MASK: usize = (1 << AL_BITS) - 1; -// Internal constants +const SL_BITS: u32 = 5; +const SL_SIZE: usize = 1 << SL_BITS; -const ALIGN_SIZE_LOG2: u32 = sizeof() == 8 ? 3 : 2; -const ALIGN_SIZE: u32 = 1 << ALIGN_SIZE_LOG2; -const FL_INDEX_MAX: u32 = sizeof() == 8 ? 32 : 30; -const SL_INDEX_COUNT: u32 = 1 << SL_INDEX_COUNT_LOG2; -const FL_INDEX_SHIFT: u32 = SL_INDEX_COUNT_LOG2 + ALIGN_SIZE_LOG2; -const FL_INDEX_COUNT: u32 = FL_INDEX_MAX - FL_INDEX_SHIFT + 1; -const SMALL_BLOCK_SIZE: u32 = 1 << FL_INDEX_SHIFT; +const SB_BITS: usize = (SL_BITS + AL_BITS); +const SB_SIZE: usize = 1 << SB_BITS; +const SB_MASK: usize = SB_SIZE - 1; -/** Block header structure. */ +const FL_BITS: u32 = (sizeof() == sizeof() + ? 30 // ^= up to 1GB per block + : 32 // ^= up to 4GB per block +) - SB_BITS; + +// ╒════════════════ Block structure layout (32-bit) ══════════════╕ +// 3 2 1 +// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 bits +// ├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┼─┼─┤ +// │ size │L│F│ ◄─┐ +// ╞═══════════════════════════════════════════════════════════╧═╧═╡ │ ┐ +// │ if free: ◄ prev │ ◄─┤ usize +// ├───────────────────────────────────────────────────────────────┤ │ +// │ if free: next ► │ ◄─┤ +// ├───────────────────────────────────────────────────────────────┤ │ +// │ ... unused free space >= 0 ... │ │ = 0 +// ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │ +// │ if free: jump ▲ │ ◄─┘ +// └───────────────────────────────────────────────────────────────┘ MIN SIZE ┘ +// F: FREE, L: LEFT_FREE + +/** Tag indicating that this block is free. */ +const FREE: usize = 1 << 0; +/** Tag indicating that this block's left block is free. */ +const LEFT_FREE: usize = 1 << 1; +/** Mask to obtain all tags. */ +const TAGS: usize = FREE | LEFT_FREE; + +assert(AL_BITS >= 2); // alignment must be large enough to store all tags + +/** Block structure. */ @unmanaged -class BlockHeader { +class Block { - /////////////////////////////// Constants /////////////////////////////////// + /** Info field holding this block's size and tags. */ + info: usize; - // One block header is 16 bytes in WASM32 and 32 bytes in WASM64. - static readonly SIZE: usize = 4 * sizeof(); + /** End offset of the {@link Block#info} field. User data starts here. */ + static readonly INFO: usize = sizeof(); - // Since block sizes are always at least a multiple of 4, the two least - // significant bits of the size field are used to store the block status. - static readonly FREE_BIT: usize = 1 << 0; - static readonly PREV_FREE_BIT: usize = 1 << 1; + /** Previous free block, if any. Only valid if free. */ + prev: Block | null; + /** Next free block, if any. Only valid if free. */ + next: Block | null; - // The size of the block header exposed to used blocks is the size field. - // The prev_phys_block field is stored *inside* the previous free block. - static readonly OVERHEAD: usize = sizeof(); + /** Minimum size of a block, excluding {@link Block#info}. */ + static readonly MIN_SIZE: usize = 3 * sizeof(); // prev + next + jump - // User data starts directly after the size field in a used block. - static readonly DATA_OFFSET: usize = sizeof() + sizeof(); + /** Maximum size of a used block, excluding {@link Block#info}. */ + static readonly MAX_SIZE: usize = 1 << (FL_BITS + SB_BITS); - // A free block must be large enough to store its header minus the size of - // the prev_phys_block field, and no larger than the number of addressable - // bits for FL_INDEX. - static readonly BLOCK_SIZE_MIN: usize = BlockHeader.SIZE - sizeof(); - static readonly BLOCK_SIZE_MAX: usize = 1 << FL_INDEX_MAX; - - ///////////////////////////////// Fields //////////////////////////////////// - - /** - * Points to the previous physical block. Only valid if the previous block is - * free. Actually stored at the end of the previous block. - */ - prev_phys_block: BlockHeader; - - /** - * The size of this block, excluding the block header. The two least - * significant bits are used to store the block status. - */ - tagged_size: usize; - - /** Next free block. Only valid if the block is free. */ - next_free: BlockHeader; - - /** Previous free block. Only valid if the block is free. */ - prev_free: BlockHeader; - - ///////////////////////////////// Methods /////////////////////////////////// - - /** Gets the size of this block, excluding the block header. */ - get size(): usize { - const tags = BlockHeader.FREE_BIT | BlockHeader.PREV_FREE_BIT; - return this.tagged_size & ~tags; + /** Gets this block's left (free) block in memory. */ + get left(): Block { + assert(this.info & LEFT_FREE); // must be free to host a jump + return assert( + load(changetype(this) - sizeof()) + ); // can't be null } - /** Sets the size of this block, retaining tagged bits. */ - set size(size: usize) { - const tags = BlockHeader.FREE_BIT | BlockHeader.PREV_FREE_BIT; - this.tagged_size = size | (this.tagged_size & tags); - } - - /** Tests if this is the last block. */ - get isLast(): bool { - return this.size == 0; - } - - /** Tests if this block's status is 'free'. */ - get isFree(): bool { - return (this.tagged_size & BlockHeader.FREE_BIT) != 0; - } - - /** Tags this block as 'free'. Careful: Does not update adjacent blocks. */ - tagAsFree(): void { - this.tagged_size |= BlockHeader.FREE_BIT; - } - - /** Tags this block as 'used'. Careful: Does not update adjacent blocks. */ - tagAsUsed(): void { - this.tagged_size &= ~BlockHeader.FREE_BIT; - } - - /** Tests if the previous block is free. */ - get isPrevFree(): bool { - return (this.tagged_size & BlockHeader.PREV_FREE_BIT) != 0; - } - - /** Tags this block as 'prev is free'. Does not update adjacent blocks. */ - tagAsPrevFree(): void { - this.tagged_size |= BlockHeader.PREV_FREE_BIT; - } - - /** Tags this block as 'prev is used'. Does not update adjacent blocks. */ - tagAsPrevUsed(): void { - this.tagged_size &= ~BlockHeader.PREV_FREE_BIT; - } - - /** Gets the block header matching the specified data pointer. */ - static fromDataPtr(ptr: usize): BlockHeader { - return changetype(ptr - BlockHeader.DATA_OFFSET); - } - - /** Returns the address of this block's data. */ - toDataPtr(): usize { - return changetype(this) + BlockHeader.DATA_OFFSET; - } - - /** Gets the next block after this one using the specified size. */ - static fromOffset(ptr: usize, size: usize): BlockHeader { - return changetype(ptr + size); - } - - /** Gets the previous block. */ - get prev(): BlockHeader { - assert(this.isPrevFree, - "previous block must be free" - ); - return this.prev_phys_block; - } - - /** Gets the next block. */ - get next(): BlockHeader { - assert(!this.isLast, - "last block has no next block" - ); - return BlockHeader.fromOffset( - this.toDataPtr(), - this.size - BlockHeader.OVERHEAD - ); - } - - /** - * Links this block with its physical next block and returns the next block. - */ - linkNext(): BlockHeader { - var next = this.next; - next.prev_phys_block = this; - return next; - } - - /** Marks this block as being 'free'. */ - markAsFree(): void { - var next = this.linkNext(); // Link the block to the next block first. - next.tagAsPrevFree(); - this.tagAsFree(); - } - - /** Marks this block as being 'used'. */ - markAsUsed(): void { - var next = this.next; - next.tagAsPrevUsed(); - this.tagAsUsed(); - } - - /** Tests if this block can be splitted. */ - canSplit(size: usize): bool { - return this.size >= BlockHeader.SIZE + size; - } - - /** Splits a block into two, the second of which is free. */ - split(size: usize): BlockHeader { - // Calculate the amount of space left in the remaining block. - var remain = BlockHeader.fromOffset( - this.toDataPtr(), - size - BlockHeader.OVERHEAD - ); - var remain_size = this.size - (size + BlockHeader.OVERHEAD); - assert(remain.toDataPtr() == align_ptr(remain.toDataPtr(), ALIGN_SIZE), - "remaining block not aligned properly" - ); - remain.size = remain_size; - assert(remain.size >= BlockHeader.BLOCK_SIZE_MIN, - "block split with invalid size" - ); - this.size = size; - remain.markAsFree(); - return remain; - } - - /** Absorb a free block's storage into this (adjacent previous) free block. */ - absorb(block: BlockHeader): void { - assert(!this.isLast, - "previous block can't be last" - ); - // Leaves tags untouched - this.tagged_size += block.size + BlockHeader.OVERHEAD; - this.linkNext(); + /** Gets this block's right block in memory. */ + get right(): Block { + assert(this.info & ~TAGS); // can't skip over the tail block + return assert( + changetype( + changetype(this) + Block.INFO + (this.info & ~TAGS) + ) + ); // can't be null } } -/** The TLSF control structure. */ -@unmanaged -class Control extends BlockHeader { // Empty lists point here, indicating free +// ╒════════════════ Root structure layout (32-bit) ═══════════════╕ +// 3 2 1 +// 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 bits +// ├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┤ ┐ +// │ 0 | flMap S│ ◄────┐ +// ╞═══════════════════════════════════════════════════════════════╡ │ +// │ slMap[0] (small blocks) │ ◄─┐ │ +// ├───────────────────────────────────────────────────────────────┤ │ │ +// │ slMap[1] │ ◄─┤ │ +// ├───────────────────────────────────────────────────────────────┤ u32 │ +// │ ... │ ◄─┤ │ +// ├───────────────────────────────────────────────────────────────┤ │ │ +// │ slMap[22] * │ ◄─┘ │ +// ╞═══════════════════════════════════════════════════════════════╡ usize +// │ head[0] │ ◄────┤ +// ├───────────────────────────────────────────────────────────────┤ │ +// │ ... │ ◄────┤ +// ├───────────────────────────────────────────────────────────────┤ │ +// │ head[736] │ ◄────┘ +// └───────────────────────────────────────────────────────────────┘ SIZE ┘ +// *: Possibly followed by padding if 64-bit - // The control structure uses 3188 bytes in WASM32. +assert((1 << SL_BITS) <= 32); // second level must fit into 32 bits + +/** Root structure. */ +@unmanaged +class Root { + + /** First level bitmap. */ + flMap: usize = 0; + + /** Start offset of second level maps. */ + private static readonly SL_START: usize = sizeof(); + + // Using *one* SL map per *FL bit* + + /** Gets the second level map for the specified first level. */ + getSLMap(fl: usize): u32 { + assert(fl < FL_BITS); // fl out of range + return load(changetype(this) + fl * 4, Root.SL_START); + } + + /** Sets the second level map for the specified first level. */ + setSLMap(fl: usize, value: u32): void { + assert(fl < FL_BITS); // fl out of range + store(changetype(this) + fl * 4, value, Root.SL_START); + } + + /** End offset of second level maps. */ + private static readonly SL_END: usize = Root.SL_START + FL_BITS * 4; + + // Using *number bits per SL* heads per *FL bit* + + /** Start offset of FL/SL heads. */ + private static readonly HL_START: usize = (Root.SL_END + AL_MASK) & ~AL_MASK; + + /** Gets the head of the specified first and second level index. */ + getHead(fl: usize, sl: u32): Block | null { + assert(fl < FL_BITS); // fl out of range + assert(sl < SL_SIZE); // sl out of range + return changetype(load( + changetype(this) + (fl * SL_SIZE + sl) * sizeof() + , Root.HL_START)); + } + + /** Sets the head of the specified first and second level index. */ + setHead(fl: usize, sl: u32, value: Block | null): void { + assert(fl < FL_BITS); // fl out of range + assert(sl < SL_SIZE); // sl out of range + store( + changetype(this) + (fl * SL_SIZE + sl) * sizeof() + , changetype(value) + , Root.HL_START); + } + + /** Total size of the {@link Root} structure. */ static readonly SIZE: usize = ( - BlockHeader.SIZE - + (1 + FL_INDEX_COUNT) * sizeof() - + FL_INDEX_COUNT * SL_INDEX_COUNT * sizeof() + Root.HL_START + FL_BITS * SL_SIZE * sizeof() ); - ///////////////////////////////// Fields //////////////////////////////////// + /** Inserts a previously used block back into the free list. */ + insert(block: Block): void { + // check as much as possible here to prevent invalid free blocks + assert(block); // cannot be null + assert(block.info & FREE); // must be free + var size: usize; + assert( + (size = block.info & ~TAGS) >= Block.MIN_SIZE && size < Block.MAX_SIZE + ); // must be valid, not necessary to compute yet if noAssert=true - /** First level free list bitmap. */ - fl_bitmap: u32; + var right: Block = assert(block.right); // can't be null - /** - * Gets the second level free list bitmap for the specified index. - * Equivalent to `sl_bitmap[fl_index]`. - */ - sl_bitmap(fl_index: u32): u32 { - const offset = BlockHeader.SIZE + sizeof(); - return load( - changetype(this) - + fl_index * sizeof() - , offset); - } - - /** - * Sets the second level free list bitmap for the specified index. - * Equivalent to `sl_bitmap[fl_index] = sl_map`. - */ - sl_bitmap_set(fl_index: u32, sl_map: u32): void { - const offset = BlockHeader.SIZE + sizeof(); - return store( - changetype(this) - + fl_index * sizeof(), - sl_map - , offset); - } - - /** - * Gets the head of the free list for the specified indexes. - * Equivalent to `blocks[fl_index][sl_index]`. - */ - blocks(fli: u32, sli: u32): BlockHeader { - const offset = BlockHeader.SIZE + (1 + FL_INDEX_COUNT) * sizeof(); - return load( - changetype(this) - + (fli * SL_INDEX_COUNT + sli) * sizeof() - , offset); - } - - /** - * Sets the head of the free list for the specified indexes. - * Equivalent to `blocks[fl_index][sl_index] = block`. - */ - blocks_set(fl_index: u32, sl_index: u32, block: BlockHeader): void { - const offset = BlockHeader.SIZE + (1 + FL_INDEX_COUNT) * sizeof(); - return store( - changetype(this) - + (fl_index * SL_INDEX_COUNT + sl_index) * sizeof(), - block - , offset); - } - - ///////////////////////////////// Methods /////////////////////////////////// - - /** Removes a given block from the free list. */ - removeBlock(block: BlockHeader): void { - mapping_insert(block.size); - this.removeFreeBlock(block, fl_out, sl_out); - } - - /** Inserts a given block into the free list. */ - insertBlock(block: BlockHeader): void { - mapping_insert(block.size); - this.insertFreeBlock(block, fl_out, sl_out); - } - - /** Inserts a free block into the free block list. */ - insertFreeBlock(block: BlockHeader, fl: i32, sl: i32): void { - var current = this.blocks(fl, sl); - assert(current, - "free list cannot have a null entry" - ); - assert(block, - "cannot insert a null entry into the free list" - ); - block.next_free = current; - block.prev_free = this; - current.prev_free = block; - assert(block.isFree, - "block must be free" - ); - assert(block.toDataPtr() == align_ptr(block.toDataPtr(), ALIGN_SIZE), - "block not aligned properly" - ); - // Insert the new block at the head of the list, and mark the first- - // and second-level bitmaps appropriately. - this.blocks_set(fl, sl, block); - this.fl_bitmap |= (1 << fl); - this.sl_bitmap_set(fl, this.sl_bitmap(fl) | (1 << sl)) - } - - /** Removes a free block from the free list.*/ - removeFreeBlock(block: BlockHeader, fl: i32, sl: i32): void { - var prev = block.prev_free; - var next = block.next_free; - assert(prev, - "prev_free field cannot be null" - ); - assert(next, - "next_free field cannot be null" - ); - next.prev_free = prev; - prev.next_free = next; - if (this.blocks(fl, sl) == block) { - this.blocks_set(fl, sl, next); - if (next == this) { - var sl_bitmap = this.sl_bitmap(fl) & ~(1 << sl); - this.sl_bitmap_set(fl, sl_bitmap); - if (!sl_bitmap) { - this.fl_bitmap &= ~(1 << fl); - } - } + // merge with right block if also free + if (right.info & FREE) { + this.remove(right); + block.info += Block.INFO + (right.info & ~TAGS); + right = block.right; + // jump is set below } - } - /** Merges a just-freed block with an adjacent previous free block. */ - mergePrevBlock(block: BlockHeader): BlockHeader { - if (block.isPrevFree) { - var prev = block.prev; - assert(prev, - "prev physical block can't be null" - ); - assert(prev.isFree, - "prev block is not free though marked as such" - ); - this.removeBlock(prev); - prev.absorb(block); - block = prev; + // merge with left block if also free + if (block.info & LEFT_FREE) { + var left: Block = assert(block.left); // can't be null + assert(left.info & FREE); // must be free according to tags + this.remove(left); + left.info += Block.INFO + (block.info & ~TAGS); + block = left; + // jump is set below } - return block; + + right.info |= LEFT_FREE; + this.setJump(block, right); + + size = block.info & ~TAGS; + assert(size >= Block.MIN_SIZE && size < Block.MAX_SIZE); // must be valid + + // mapping_insert + var fl: usize, sl: u32; + if (size < SB_SIZE) { + fl = 0; + sl = (size / AL_SIZE); + } else { + fl = fls(size); + sl = ((size >> (fl - SL_BITS)) ^ (1 << SL_BITS)); + fl -= SB_BITS - 1; + } + + // perform insertion + var head = this.getHead(fl, sl); + block.prev = null; + block.next = head; + if (head) head.prev = block; + this.setHead(fl, sl, block); + + // update first and second level maps + this.flMap |= (1 << fl); + this.setSLMap(fl, this.getSLMap(fl) | (1 << sl)); } - /** Merges a just-freed block with an adjacent free block. */ - mergeNextBlock(block: BlockHeader): BlockHeader { + /** + * Removes a free block from FL/SL maps. Does not alter left/jump because it + * is likely that splitting is performed afterwards, invalidating any changes + * again. + */ + private remove(block: Block): void { + assert(block.info & FREE); // must be free + var size = block.info & ~TAGS; + assert(size >= Block.MIN_SIZE && size < Block.MAX_SIZE); // must be valid + + // mapping_insert + var fl: usize, sl: u32; + if (size < SB_SIZE) { + fl = 0; + sl = (size / AL_SIZE); + } else { + fl = fls(size); + sl = ((size >> (fl - SL_BITS)) ^ (1 << SL_BITS)); + fl -= SB_BITS - 1; + } + + // link previous and next free block + var prev = block.prev; var next = block.next; - assert(next, - "next physical block can't be null" - ); - if (next.isFree) { - assert(!block.isLast, - "previous block can't be last" - ); - this.removeBlock(next); - block.absorb(next); - } - return block; - } + if (prev) + prev.next = next; + if (next) + next.prev = prev; - /** - * Trims any trailing block space off the end of a block and returns it to - * the pool. */ - trimFreeBlock(block: BlockHeader, size: usize): void { - assert(block.isFree, - "block must be free" - ); - if (block.canSplit(size)) { - var remaining_block = block.split(size); - block.linkNext(); - remaining_block.tagAsPrevFree(); - this.insertBlock(remaining_block); - } - } + // update head if we are removing it + if (block == this.getHead(fl, sl)) { + this.setHead(fl, sl, next); - /** - * Trims any trailing block space off the end of a used block and returns it - * to the pool. - */ - trimUsedBlock(block: BlockHeader, size: usize): void { - assert(!block.isFree, - "block must be used" - ); - if (block.canSplit(size)) { - // If the next block is free, we must coalesce. - var remaining_block = block.split(size); - remaining_block.tagAsPrevUsed(); - remaining_block = this.mergeNextBlock(remaining_block); - this.insertBlock(remaining_block); - } - } + // clear second level map if head is empty now + if (!next) { + var slMap = this.getSLMap(fl); + this.setSLMap(fl, slMap &= ~(1 << sl)); - trimFreeBlockLeading(block: BlockHeader, size: usize): BlockHeader { - var remaining_block = block; - if (block.canSplit(size)) { - remaining_block = block.split(size - BlockHeader.OVERHEAD); - remaining_block.tagAsPrevFree(); - block.linkNext(); - this.insertBlock(block); - } - return remaining_block; - } - - locateFreeBlock(size: usize): BlockHeader { - var block = changetype(0); - if (size) { - mapping_search(size); - if (fl_out < FL_INDEX_MAX) { - block = find_suitable_block(this, fl_out, sl_out); + // clear first level map if second level is empty now + if (!slMap) + this.flMap &= ~(1 << fl); } } - if (block) { - assert(block.size >= size); - this.removeFreeBlock(block, fl_out, sl_out); - } - return block; } - prepareUsedBlock(block: BlockHeader, size: usize): usize { - var ptr: usize = 0; - if (block) { - assert(size, - "size must be non-zero" - ); - this.trimFreeBlock(block, size); - block.markAsUsed(); - ptr = block.toDataPtr(); - } - return ptr; - } + /** Searches for a free block of at least the specified size. */ + search(size: usize): Block | null { + assert(size >= Block.MIN_SIZE && size < Block.MAX_SIZE); - /** - * Creates a TLSF control structure at the specified memory address, - * providing the specified number of bytes. - */ - static create(mem: usize, bytes: usize): Control { - if ((mem % ALIGN_SIZE) != 0) - throw new RangeError("Memory must be aligned"); - - // Clear structure and point all empty lists at the null block. - var control = changetype(mem); - control.next_free = control; - control.prev_free = control; - control.fl_bitmap = 0; - for (var i = 0; i < FL_INDEX_COUNT; ++i) { - control.sl_bitmap_set(i, 0); - for (var j = 0; j < SL_INDEX_COUNT; ++j) { - control.blocks_set(i, j, control); - } + // mapping_search + var fl: usize, sl: u32; + if (size < SB_SIZE) { + fl = 0; + sl = (size / AL_SIZE); + } else { + // (*) size += (1 << (fls(size) - SL_BITS)) - 1; + fl = fls(size); + sl = ((size >> (fl - SL_BITS)) ^ (1 << SL_BITS)); + fl -= SB_BITS - 1; + // (*) instead of rounding up, use next second level list for better fit + if (sl < SL_SIZE - 1) ++sl; + else ++fl, sl = 0; } - // Add the initial memory pool - control.addPool(mem + Control.SIZE, bytes - Control.SIZE); - return control; - } - - /** Adds a pool of free memory. */ - addPool(mem: usize, bytes: usize): void { - // Overhead of the TLSF structures in a given memory block, equal - // to the overhead of the free block and the sentinel block. - const pool_overhead = BlockHeader.OVERHEAD * 2; - - var pool_bytes = align_down(bytes - pool_overhead, ALIGN_SIZE); - if ((mem % ALIGN_SIZE) != 0) - throw new RangeError("Memory must be aligned"); - if (pool_bytes < BlockHeader.BLOCK_SIZE_MIN || - pool_bytes > BlockHeader.BLOCK_SIZE_MAX) - throw new RangeError("Memory size must be between min and max"); - - // Create the main free block. Offset the start of the block slightly - // so that the prev_phys_block field falls outside of the pool - - // it will never be used. - var block = BlockHeader.fromOffset(mem, -BlockHeader.OVERHEAD); - block.size = pool_bytes; - block.tagAsFree(); - block.tagAsPrevUsed(); - this.insertBlock(block); - - // Split the block to create a zero-size sentinel block. - var next = block.linkNext(); - next.size = 0; - next.tagAsUsed(); - next.tagAsPrevFree(); - } -} - -// Alignment helpers - -function align_up(x: usize, align: usize): usize { - assert(!(align & (align - 1)), - "must align to a power of two" - ); - return (x + (align - 1)) & ~(align - 1); -} - -function align_down(x: usize, align: usize): usize { - assert(!(align & (align - 1)), - "must align to a power of two" - ); - return x - (x & (align - 1)); -} - -function align_ptr(ptr: usize, align: usize): usize { - var aligned = (ptr + (align - 1)) & ~(align - 1); - assert(!(align & (align - 1)), - "must align to a power of two" - ); - return aligned; -} - -/** - * Adjusts an allocation size to be aligned to word size, and no smaller than - * the internal minimum. - */ -function adjust_request_size(size: usize, align: usize): usize { - var adjust: usize = 0; - if (size && size < BlockHeader.BLOCK_SIZE_MAX) { - var aligned = align_up(size, align); - adjust = max(aligned, BlockHeader.BLOCK_SIZE_MIN); - } - return adjust; -} - -// TLSF utility functions. In most cases, these are direct translations of the -// documentation found in the white paper. - -function ffs(word: i32): i32 { - return word ? ctz(word) : -1; -} - -function fls(word: T): i32 { - return (sizeof() << 3) - 1 - clz(word); -} - -let fl_out: i32, sl_out: i32; - -function mapping_insert(size: usize): void { - var fl: i32, sl: i32; - if (size < SMALL_BLOCK_SIZE) { // Store small blocks in first list. - fl = 0; - sl = size / (SMALL_BLOCK_SIZE / SL_INDEX_COUNT); - } else { - fl = fls(size); - sl = (size >> (fl - SL_INDEX_COUNT_LOG2)) ^ 1 << SL_INDEX_COUNT_LOG2; - fl -= FL_INDEX_SHIFT - 1; - } - fl_out = fl; - sl_out = sl; -} - -function mapping_search(size: usize): void { - if (size >= SMALL_BLOCK_SIZE) - size += (1 << (fls(size) - SL_INDEX_COUNT_LOG2)) - 1; - mapping_insert(size); -} - -function find_suitable_block(control: Control, fl: i32, sl: i32): BlockHeader { - // Search for a block in the list associated with the given fl/sl index - var sl_map = control.sl_bitmap(fl) & (~0 << sl); - if (!sl_map) { - // If no block exists, search in the next largest first-level list - var fl_map = control.fl_bitmap & (~0 << (fl + 1)); - if (!fl_map) - return changetype(0); // Memory pool has been exhausted - fl = ctz(fl_map); // ^= ffs(fl_map) with fl_map != 0 - fl_out = fl; - sl_map = control.sl_bitmap(fl); - } - assert(sl_map, - "second level bitmap is null" - ); - sl = ctz(sl_map); // ^= ffs(sl_map) with sl_map != 0 - sl_out = sl; - return control.blocks(fl, sl); // First block in the free list -} - -// Exported interface - -let TLSF: Control; - -/** Requests more memory from the host environment. */ -function request_memory(size: usize): void { - if (size & 0xffff) // Round size up to a full page - size = (size | 0xffff) + 1; - // At least double the memory for efficiency - var prev_pages = grow_memory(max(current_memory(), size >> 16)); - if (prev_pages < 0) - unreachable(); // Out of host memory. This is bad. - var next_pages = current_memory(); - TLSF.addPool(prev_pages << 16, (next_pages - prev_pages) << 16); -} - -/** Allocates a chunk of memory of the given size and returns its address. */ -export function allocate_memory(size: usize): usize { - if (!TLSF) // Initialize when actually used so it DCEs just fine otherwise - TLSF = Control.create(HEAP_BASE, (current_memory() << 16) - HEAP_BASE); - var control = changetype(TLSF); - var adjust = adjust_request_size(size, ALIGN_SIZE); - var block = control.locateFreeBlock(adjust); - if (!block && size > 0) { - request_memory(adjust); - block = control.locateFreeBlock(adjust); - } - return control.prepareUsedBlock(block, adjust); -} - -/** Disposes a chunk of memory by its pointer. */ -export function free_memory(ptr: usize): void { - if (TLSF && ptr) { - var control = changetype(TLSF); - var block = BlockHeader.fromDataPtr(ptr); - assert(!block.isFree, - "block already marked as free" - ); - block.markAsFree(); - block = control.mergePrevBlock(block); - block = control.mergeNextBlock(block); - control.insertBlock(block); - } -} - -// Extra debugging - -assert(sizeof() << 3 >= SL_INDEX_COUNT, - "SL_INDEX_COUNT must be <= number of bits in sl_bitmap's storage type" -); -assert(ALIGN_SIZE == SMALL_BLOCK_SIZE / SL_INDEX_COUNT, - "invalid alignment" -); -assert(test_ffs_fls() == 0, - "ffs/fls are not working properly" -); - -function test_ffs_fls(): i32 { - var rv = 0; - rv += (ffs(0) == -1) ? 0 : 0x1; - rv += (fls(0) == -1) ? 0 : 0x2; - rv += (ffs(1) == 0) ? 0 : 0x4; - rv += (fls(1) == 0) ? 0 : 0x8; - rv += (ffs(0x80000000) == 31) ? 0 : 0x10; - rv += (ffs(0x80008000) == 15) ? 0 : 0x20; - rv += (fls(0x80000008) == 31) ? 0 : 0x40; - rv += (fls(0x7FFFFFFF) == 30) ? 0 : 0x80; - if (sizeof() == 8) { - rv += (fls(0x80000000) == 31) ? 0 : 0x100; - rv += (fls(0x100000000) == 32) ? 0 : 0x200; - rv += (fls(0xffffffffffffffff) == 63) ? 0 : 0x400; - } - return rv; -} - -function check(): i32 { - if (!TLSF) - TLSF = Control.create(HEAP_BASE, (current_memory() << 16) - HEAP_BASE); - var control = changetype(TLSF); - var status = 0; - for (var i = 0; i < FL_INDEX_COUNT; ++i) { - for (var j = 0; j < SL_INDEX_COUNT; ++j) { - var fl_map = control.fl_bitmap & (1 << i); - var sl_list = control.sl_bitmap(i); - var sl_map = sl_list & (1 << j); - var block = control.blocks(i, j); - if (!fl_map) { - if (!assert(!sl_map, - "second-level map must be null") - ) --status; - } - if (!sl_map) { - if (!assert(block == control, - "block list must be null") - ) --status; + // search second level + var slMap = this.getSLMap(fl) & (~0 << sl); + var head: Block | null; + if (!slMap) { + // search next larger first level + var flMap = this.flMap & (~0 << (fl + 1)); + if (!flMap) { + head = null; } else { - if (!assert(sl_list, - "no free blocks in second-level map") - ) --status; - if (!assert(block != control, - "block should not be null") - ) --status; - while (block != control) { - if (!assert(block.isFree, - "block should be free") - ) --status; - if (!assert(!block.isPrevFree, - "blocks should have coalesced") - ) --status; - if (!assert(!block.next.isFree, - "blocks should have coalesced") - ) --status; - if (!assert(block.next.isPrevFree, - "block should be free") - ) --status; - if (!assert(block.size >= BlockHeader.BLOCK_SIZE_MIN, - "block < minimum size") - ) --status; - mapping_insert(block.size); - if (!assert(fl_out == i && sl_out == j, - "block size indexed in wrong list") - ) --status; - block = block.next_free; - } + fl = ffs(flMap); + slMap = assert(this.getSLMap(fl)); // can't be zero if fl points here + head = this.getHead(fl, ffs(slMap)); } + } else + head = this.getHead(fl, ffs(slMap)); + + return head; + } + + /** Links a free left with its right block in memory. */ + private setJump(left: Block, right: Block): void { + assert(left.info & FREE); // must be free + assert(left.right == right); // right block must match + assert(right.info & LEFT_FREE); // right block must be tagged as LEFT_FREE + store( + changetype(right) - sizeof() + , left); // last word in left block's (free) data region + } + + /** + * Uses the specified free block, removing it from internal maps and + * splitting it if possible. + */ + use(block: Block, size: usize): void { + assert(block.info & FREE); // must be free so we can use it + assert(size >= Block.MIN_SIZE && size < Block.MAX_SIZE); // must be valid + assert(!(size & AL_MASK)); // size must be aligned so the new block is + + this.remove(block); + block.info &= ~FREE; + + // split if the block can hold another MIN_SIZE block + var remaining = (block.info & ~TAGS) - size; + if (remaining >= Block.INFO + Block.MIN_SIZE) { + block.info = size | (block.info & TAGS); + + var spare = changetype( + changetype(block) + Block.INFO + size + ); + spare.info = (remaining - Block.INFO) | FREE; // not LEFT_FREE + this.insert(spare); // also sets jump + + // otherwise just tag right block as no longer LEFT_FREE + } else { + var right: Block = assert(block.right); // can't be null (tail) + right.info &= ~LEFT_FREE; } } - return status; -} -let integrity_prev_status: i32, - integrity_status: i32; + /** Adds more memory to the pool. */ + addMemory(start: usize, end: usize): bool { + start = (start + AL_MASK) & ~AL_MASK; + end -= end & AL_MASK; -function integrity_walker(ptr: usize, size: usize, used: bool): void { - var block = BlockHeader.fromDataPtr(ptr); - var this_prev_status = block.isPrevFree; - var this_status = block.isFree; - var this_block_size = block.size; + // TODO: merge with current tail if adjacent - var status = 0; - if (!assert(integrity_prev_status == this_prev_status, - "prev status incorrect") - ) --status; - if (!assert(size == this_block_size, - "block size incorrect") - ) --status; - integrity_prev_status = this_status; - integrity_status += status; -} + assert(start <= end); // to be sure -function check_pool(pool: usize): i32 { - if (pool < 0x10000) { // first pool - pool = changetype(TLSF) + Control.SIZE; + // check if size is large enough for a free block and the tail block + var size = end - start; + if (size < Block.INFO + Block.MIN_SIZE + Block.INFO) + return false; + + // left size is total minus its own and the zero-length tail's header + var leftSize = size - 2 * Block.INFO; + var left = changetype(start); + left.info = leftSize | FREE; + left.prev = null; + left.next = null; + + // tail is a zero-length used block + var tail = changetype(start + size - Block.INFO); + tail.info = 0 | LEFT_FREE; + + this.insert(left); // also sets jump + + return true; } - // inlined walk_bool with static integrity_walker - integrity_prev_status = integrity_status = 0; - var block = BlockHeader.fromOffset(pool, -BlockHeader.OVERHEAD); - while (block && !block.isLast) { - integrity_walker( - block.toDataPtr(), - block.size, - !block.isFree - ); - block = block.next; - } - return integrity_status; } -export { check, check_pool }; // Uncomment to enable in tests/index.js +/** Determines the first (LSB to MSB) set bit's index of a word. */ +function ffs(word: T): T { + assert(word != 0); // word cannot be 0 + return ctz(word); // differs from ffs only for 0 +} + +/** Determins the last (LSB to MSB) set bit's index of a word. */ +function fls(word: T): T { + assert(word != 0); // word cannot be 0 + const inv: T = (sizeof() << 3) - 1; + return inv - clz(word); +} + +/** Reference to the initialized {@link Root} structure, once initialized. */ +var ROOT: Root = changetype(0); + +// External interface + +/** Allocates a chunk of memory. */ +export function allocate_memory(size: usize): usize { + + // initialize if necessary + var root = ROOT; + if (!root) { + var rootOffset = (HEAP_BASE + AL_MASK) & ~AL_MASK; + ROOT = root = changetype(rootOffset); + root.flMap = 0; + for (var fl = 0; fl < FL_BITS; ++fl) { + root.setSLMap(fl, 0); + for (var sl = 0; sl < SL_SIZE; ++sl) + root.setHead(fl, sl, null); + } + root.addMemory(rootOffset + Root.SIZE, current_memory() << 16); + } + + // search for a suitable block + var data: usize = 0; + if (size && size < Block.MAX_SIZE) { + size = max((size + AL_MASK) & ~AL_MASK, Block.MIN_SIZE); + + var block = root.search(size); + if (!block) { + + // request more memory + var pagesBefore = current_memory(); + var pagesWanted = max(pagesBefore, ((size + 0xffff) & ~0xffff) >>> 16); + if (grow_memory(pagesWanted) < 0) + unreachable(); // out of memory + var pagesAfter = current_memory(); + root.addMemory(pagesBefore << 16, pagesAfter << 16); + block = assert(root.search(size)); // must be found now + } + + assert((block.info & ~TAGS) >= size); + root.use(block, size); + data = changetype(block) + Block.INFO; + } + + return data; +} + +/** Frees the chunk of memory at the specified address. */ +export function free_memory(data: usize): void { + var root = ROOT; + if (root && data) { + var block = changetype(data - Block.INFO); + assert(!(block.info & FREE)); // must be used + block.info |= FREE; + root.insert(changetype(data - Block.INFO)); + } +} + +export { set_memory }; diff --git a/examples/tlsf/package.json b/examples/tlsf/package.json index eab049eb..9711f994 100644 --- a/examples/tlsf/package.json +++ b/examples/tlsf/package.json @@ -4,8 +4,8 @@ "private": true, "scripts": { "build": "npm run build:untouched && npm run build:optimized", - "build:untouched": "asc assembly/tlsf.ts -t tlsf.untouched.wast -b tlsf.untouched.wasm --validate", - "build:optimized": "asc -O3 assembly/tlsf.ts -b tlsf.optimized.wasm -t tlsf.optimized.wast --validate --noDebug --noAssert", + "build:untouched": "asc assembly/tlsf.ts -t tlsf.untouched.wast -b tlsf.untouched.wasm --validate --sourceMap --measure", + "build:optimized": "asc -O3 assembly/tlsf.ts -b tlsf.optimized.wasm -t tlsf.optimized.wast --validate --noDebug --noAssert --sourceMap --measure", "test": "node tests" } } diff --git a/examples/tlsf/tests/index.js b/examples/tlsf/tests/index.js index 73caedd0..848ca667 100644 --- a/examples/tlsf/tests/index.js +++ b/examples/tlsf/tests/index.js @@ -1,61 +1,28 @@ var fs = require("fs"); -function test(file) { - console.log("Testing '" + file + "' ..."); +var runner = require("./runner"); - var tlsf = new WebAssembly.Instance(WebAssembly.Module(fs.readFileSync(__dirname + "/../" + file)), { +function test(file) { + console.log("Testing '" + file + "' ...\n"); + + var exports = new WebAssembly.Instance(WebAssembly.Module(fs.readFileSync(__dirname + "/../" + file)), { env: { - log_i: function(i) { i == -1 ? console.log("---") : console.log("log_i -> " + i); } + abort: function(msg, file, line, column) { + throw Error("Assertion failed: " + (msg ? "'" + getString(msg) + "' " : "") + "at " + getString(file) + ":" + line + ":" + column); + }, + log: function(ptr) { console.log(getString(ptr)); }, + logi: function(i) { console.log(i); } } }).exports; - try { - var memSize = 0; - var ptr = 0; - for (var j = 0; j < 10000; ++j) { - if (!j || !((j + 1) % 1000)) - console.log("run #" + (j + 1)); - ptr; - var ptrs = []; - // allocate some blocks of unusual sizes - for (var i = 0; i < 2048; ++i) { - var size = i * 61; - ptr = tlsf.allocate_memory(size); - if (tlsf.set_memory) - tlsf.set_memory(ptr, ptr % 256, size); // slow - // immediately free every 4th - if (!(i % 4)) { - tlsf.free_memory(ptr); - } else { - ptrs.push(ptr); - // randomly free random blocks (if not the first run that determines max memory) - if (j && Math.random() < 0.25) { - ptr = ptrs.splice((Math.random() * ptrs.length)|0, 1)[0]; - tlsf.free_memory(ptr); - } - } - } - if (tlsf.check) - tlsf.check(); - if (tlsf.check_pool) - tlsf.check_pool(0); - // clean up by randomly freeing all blocks - while (ptrs.length) { - ptr = ptrs.splice((Math.random() * ptrs.length)|0, 1)[0]; - tlsf.free_memory(ptr); - } - if (memSize && memSize != tlsf.memory.buffer.byteLength) - throw new Error("memory should not grow multiple times: " + memSize + " != " + tlsf.memory.buffer.byteLength); - memSize = tlsf.memory.buffer.byteLength; - if (tlsf.check) - tlsf.check(); - if (tlsf.check_pool) - tlsf.check_pool(0); - } - } finally { - // mem(tlsf.memory, 0, 4096); - console.log("memSize=" + memSize); + function getString(ptr) { + var len = new Uint32Array(exports.memory.buffer, ptr)[0]; + var str = new Uint16Array(exports.memory.buffer, ptr + 4).subarray(0, len); + return String.fromCharCode.apply(String, str); } + + runner(exports, 10, 20000); // picked so I/O isn't the bottleneck + console.log("mem final: " + exports.memory.buffer.byteLength); console.log(); } diff --git a/examples/tlsf/tests/runner.js b/examples/tlsf/tests/runner.js new file mode 100644 index 00000000..efe22060 --- /dev/null +++ b/examples/tlsf/tests/runner.js @@ -0,0 +1,91 @@ +function runner(tlsf, runs, allocs) { + var ptrs = []; + + function randomAlloc(maxSize) { + if (!maxSize) maxSize = 8192; + var size = ((Math.random() * maxSize) >>> 0) + 1; + size = (size + 3) & ~3; + var ptr = tlsf.allocate_memory(size); + if (!ptr) throw Error(); + if (ptrs.indexOf(ptr) >= 0) throw Error(); + if (tlsf.set_memory) + tlsf.set_memory(ptr, 0xdc, size); + ptrs.push(ptr); + return ptr; + } + + function preciseFree(ptr) { + var idx = ptrs.indexOf(ptr); + if (idx < 0) throw Error(); + var ptr = ptrs[idx]; + ptrs.splice(idx, 1); + if (typeof ptr !== "number") throw Error(); + tlsf.free_memory(ptr); + } + + function randomFree() { + var idx = (Math.random() * ptrs.length) >>> 0; + var ptr = ptrs[idx]; + if (typeof ptr !== "number") throw Error(); + ptrs.splice(idx, 1); + tlsf.free_memory(ptr); + } + + // remember the smallest possible memory address + var base = tlsf.allocate_memory(64); + console.log("base: " + base); + tlsf.free_memory(base); + console.log("mem initial: " + tlsf.memory.buffer.byteLength); + + for (var j = 0; j < runs; ++j) { + console.log("run " + (j + 1) + " (" + allocs + " allocations) ..."); + for (var i = 0; i < allocs; ++i) { + var ptr = randomAlloc(); + + // immediately free every 4th + if (!(i % 4)) preciseFree(ptr); + + // occasionally free random blocks + else if (ptrs.length && Math.random() < 0.33) randomFree(); + + // ^ sums up to clearing about half the blocks half-way + } + // free the rest, randomly + while (ptrs.length) randomFree(); + + // should now be possible to reuse the entire first page (remember: sl+1) + // e.g. with base 3088 (3048 optimized due to static memory): + var size = 0x10000 - base - 4 - 1008; + // 61436 (1110111111111100b) -> fl = 15, sl = 27 + // 61437 (61440 aligned, 1111000000000000b) -> fl = 15, sl = 28 + // NOTE that this calculation will be different if static memory changes + var ptr = tlsf.allocate_memory(size); + tlsf.set_memory(ptr, 0xac, size); + if (ptr !== base) throw Error("expected " + base + " but got " + ptr); + tlsf.free_memory(ptr); + } + + mem(tlsf.memory, 0, 0x10000); // should end in 02 00 00 00 (tail LEFT_FREE) +} + +function mem(memory, offset, count) { + if (!offset) offset = 0; + if (!count) count = 1024; + var mem = new Uint8Array(memory.buffer, offset); + var stackTop = new Uint32Array(memory.buffer, 4, 1)[0]; + var hex = []; + for (var i = 0; i < count; ++i) { + var o = (offset + i).toString(16); + while (o.length < 4) o = "0" + o; + if ((i & 15) === 0) { + hex.push("\n" + o + ":"); + } + var h = mem[i].toString(16); + if (h.length < 2) h = "0" + h; + hex.push(h); + } + console.log(hex.join(" ") + " ..."); +} + +if (typeof module === "object" && typeof exports === "object") + module.exports = runner;