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
-=====================
+ 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;