mirror of
https://github.com/fluencelabs/assemblyscript
synced 2025-06-12 22:41:27 +00:00
Unify allocator tests even more, also test arena
This commit is contained in:
3
tests/allocators/arena/assembly/index.ts
Normal file
3
tests/allocators/arena/assembly/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import "allocator/arena";
|
||||
export { allocate_memory, free_memory, reset_memory };
|
||||
// export { set_memory };
|
6
tests/allocators/arena/assembly/package.json
Normal file
6
tests/allocators/arena/assembly/package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../../../std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
151
tests/allocators/arena/optimized.wat
Normal file
151
tests/allocators/arena/optimized.wat
Normal file
@ -0,0 +1,151 @@
|
||||
(module
|
||||
(type $ii (func (param i32) (result i32)))
|
||||
(type $iv (func (param i32)))
|
||||
(type $v (func))
|
||||
(global "$(lib)/allocator/arena/OFFSET" (mut i32) (i32.const 0))
|
||||
(global $HEAP_BASE i32 (i32.const 4))
|
||||
(memory $0 1)
|
||||
(export "allocate_memory" (func "$(lib)/allocator/arena/allocate_memory"))
|
||||
(export "free_memory" (func "$(lib)/allocator/arena/free_memory"))
|
||||
(export "reset_memory" (func "$(lib)/allocator/arena/reset_memory"))
|
||||
(export "memory" (memory $0))
|
||||
(start $(lib)/allocator/arena/reset_memory)
|
||||
(func "$(lib)/allocator/arena/allocate_memory" (; 0 ;) (type $ii) (param $0 i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(local $3 i32)
|
||||
(local $4 i32)
|
||||
;;@ (lib)/allocator/arena.ts:16:2
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:16:6
|
||||
(i32.eqz
|
||||
;;@ (lib)/allocator/arena.ts:16:7
|
||||
(get_local $0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:16:20
|
||||
(return
|
||||
(i32.const 0)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:20:2
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:20:6
|
||||
(i32.gt_u
|
||||
;;@ (lib)/allocator/arena.ts:18:2
|
||||
(tee_local $2
|
||||
;;@ (lib)/allocator/arena.ts:18:15
|
||||
(i32.and
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:18:16
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:17:2
|
||||
(tee_local $1
|
||||
;;@ (lib)/allocator/arena.ts:17:12
|
||||
(get_global "$(lib)/allocator/arena/OFFSET")
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:18:22
|
||||
(get_local $0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:18:29
|
||||
(i32.const 7)
|
||||
)
|
||||
(i32.const -8)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:20:15
|
||||
(i32.shl
|
||||
;;@ (lib)/allocator/arena.ts:19:2
|
||||
(tee_local $0
|
||||
;;@ (lib)/allocator/arena.ts:19:20
|
||||
(current_memory)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:20:37
|
||||
(i32.const 16)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:23:4
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:23:8
|
||||
(i32.lt_s
|
||||
(grow_memory
|
||||
;;@ (lib)/allocator/arena.ts:22:22
|
||||
(select
|
||||
;;@ (lib)/allocator/arena.ts:22:26
|
||||
(get_local $0)
|
||||
(tee_local $4
|
||||
;;@ (lib)/allocator/arena.ts:21:4
|
||||
(tee_local $3
|
||||
;;@ (lib)/allocator/arena.ts:21:22
|
||||
(i32.shr_u
|
||||
(i32.and
|
||||
;;@ (lib)/allocator/arena.ts:21:23
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:21:24
|
||||
(i32.sub
|
||||
(get_local $2)
|
||||
;;@ (lib)/allocator/arena.ts:21:33
|
||||
(get_local $1)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:21:39
|
||||
(i32.const 65535)
|
||||
)
|
||||
(i32.const -65536)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:21:62
|
||||
(i32.const 16)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.gt_s
|
||||
(get_local $0)
|
||||
(get_local $4)
|
||||
)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:23:35
|
||||
(i32.const 0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:24:6
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:24:10
|
||||
(i32.lt_s
|
||||
(grow_memory
|
||||
;;@ (lib)/allocator/arena.ts:24:22
|
||||
(get_local $3)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:24:37
|
||||
(i32.const 0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:25:8
|
||||
(unreachable)
|
||||
)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:29:2
|
||||
(set_global "$(lib)/allocator/arena/OFFSET"
|
||||
;;@ (lib)/allocator/arena.ts:29:11
|
||||
(get_local $2)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:30:9
|
||||
(get_local $1)
|
||||
)
|
||||
(func "$(lib)/allocator/arena/free_memory" (; 1 ;) (type $iv) (param $0 i32)
|
||||
;;@ (lib)/allocator/arena.ts:34:46
|
||||
(nop)
|
||||
)
|
||||
(func "$(lib)/allocator/arena/reset_memory" (; 2 ;) (type $v)
|
||||
;;@ (lib)/allocator/arena.ts:40:2
|
||||
(set_global "$(lib)/allocator/arena/OFFSET"
|
||||
;;@ (lib)/allocator/arena.ts:40:11
|
||||
(i32.and
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:40:12
|
||||
(get_global $HEAP_BASE)
|
||||
;;@ (lib)/allocator/arena.ts:40:24
|
||||
(i32.const 7)
|
||||
)
|
||||
(i32.const -8)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
8
tests/allocators/arena/package.json
Normal file
8
tests/allocators/arena/package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run build:untouched && npm run build:optimized",
|
||||
"build:untouched": "asc assembly/index.ts -t untouched.wat -b untouched.wasm --validate --sourceMap --measure",
|
||||
"build:optimized": "asc assembly/index.ts -t optimized.wat -b optimized.wasm --validate --sourceMap --measure --noDebug --noAssert --optimize"
|
||||
}
|
||||
}
|
207
tests/allocators/arena/untouched.wat
Normal file
207
tests/allocators/arena/untouched.wat
Normal file
@ -0,0 +1,207 @@
|
||||
(module
|
||||
(type $i (func (result i32)))
|
||||
(type $ii (func (param i32) (result i32)))
|
||||
(type $iv (func (param i32)))
|
||||
(type $v (func))
|
||||
(global "$(lib)/allocator/arena/AL_BITS" i32 (i32.const 3))
|
||||
(global "$(lib)/allocator/arena/AL_SIZE" i32 (i32.const 8))
|
||||
(global "$(lib)/allocator/arena/AL_MASK" i32 (i32.const 7))
|
||||
(global "$(lib)/allocator/arena/OFFSET" (mut i32) (i32.const 0))
|
||||
(global $HEAP_BASE i32 (i32.const 4))
|
||||
(memory $0 1)
|
||||
(export "allocate_memory" (func "$(lib)/allocator/arena/allocate_memory"))
|
||||
(export "free_memory" (func "$(lib)/allocator/arena/free_memory"))
|
||||
(export "reset_memory" (func "$(lib)/allocator/arena/reset_memory"))
|
||||
(export "memory" (memory $0))
|
||||
(start $start)
|
||||
(func "$(lib)/allocator/arena/allocate_memory" (; 0 ;) (type $ii) (param $0 i32) (result i32)
|
||||
(local $1 i32)
|
||||
(local $2 i32)
|
||||
(local $3 i32)
|
||||
(local $4 i32)
|
||||
(local $5 i32)
|
||||
(local $6 i32)
|
||||
(local $7 i32)
|
||||
;;@ (lib)/allocator/arena.ts:16:2
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:16:6
|
||||
(i32.eqz
|
||||
;;@ (lib)/allocator/arena.ts:16:7
|
||||
(get_local $0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:16:20
|
||||
(return
|
||||
(i32.const 0)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:17:2
|
||||
(set_local $1
|
||||
;;@ (lib)/allocator/arena.ts:17:12
|
||||
(get_global "$(lib)/allocator/arena/OFFSET")
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:18:2
|
||||
(set_local $2
|
||||
;;@ (lib)/allocator/arena.ts:18:15
|
||||
(i32.and
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:18:16
|
||||
(i32.add
|
||||
(get_local $1)
|
||||
;;@ (lib)/allocator/arena.ts:18:22
|
||||
(get_local $0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:18:29
|
||||
(i32.const 7)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:18:40
|
||||
(i32.xor
|
||||
;;@ (lib)/allocator/arena.ts:18:41
|
||||
(i32.const 7)
|
||||
(i32.const -1)
|
||||
)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:19:2
|
||||
(set_local $3
|
||||
;;@ (lib)/allocator/arena.ts:19:20
|
||||
(current_memory)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:20:2
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:20:6
|
||||
(i32.gt_u
|
||||
(get_local $2)
|
||||
;;@ (lib)/allocator/arena.ts:20:15
|
||||
(i32.shl
|
||||
(get_local $3)
|
||||
;;@ (lib)/allocator/arena.ts:20:37
|
||||
(i32.const 16)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:20:41
|
||||
(block
|
||||
;;@ (lib)/allocator/arena.ts:21:4
|
||||
(set_local $4
|
||||
;;@ (lib)/allocator/arena.ts:21:22
|
||||
(i32.shr_u
|
||||
(i32.and
|
||||
;;@ (lib)/allocator/arena.ts:21:23
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:21:24
|
||||
(i32.sub
|
||||
(get_local $2)
|
||||
;;@ (lib)/allocator/arena.ts:21:33
|
||||
(get_local $1)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:21:39
|
||||
(i32.const 65535)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:21:49
|
||||
(i32.xor
|
||||
;;@ (lib)/allocator/arena.ts:21:50
|
||||
(i32.const 65535)
|
||||
(i32.const -1)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:21:62
|
||||
(i32.const 16)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:22:4
|
||||
(set_local $7
|
||||
;;@ (lib)/allocator/arena.ts:22:22
|
||||
(select
|
||||
(tee_local $5
|
||||
;;@ (lib)/allocator/arena.ts:22:26
|
||||
(get_local $3)
|
||||
)
|
||||
(tee_local $6
|
||||
;;@ (lib)/allocator/arena.ts:22:39
|
||||
(get_local $4)
|
||||
)
|
||||
(i32.gt_s
|
||||
(get_local $5)
|
||||
(get_local $6)
|
||||
)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:23:4
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:23:8
|
||||
(i32.lt_s
|
||||
(grow_memory
|
||||
;;@ (lib)/allocator/arena.ts:23:20
|
||||
(get_local $7)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:23:35
|
||||
(i32.const 0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:24:6
|
||||
(if
|
||||
;;@ (lib)/allocator/arena.ts:24:10
|
||||
(i32.lt_s
|
||||
(grow_memory
|
||||
;;@ (lib)/allocator/arena.ts:24:22
|
||||
(get_local $4)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:24:37
|
||||
(i32.const 0)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:25:8
|
||||
(unreachable)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:29:2
|
||||
(set_global "$(lib)/allocator/arena/OFFSET"
|
||||
;;@ (lib)/allocator/arena.ts:29:11
|
||||
(get_local $2)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:30:9
|
||||
(return
|
||||
(get_local $1)
|
||||
)
|
||||
)
|
||||
(func "$(lib)/allocator/arena/free_memory" (; 1 ;) (type $iv) (param $0 i32)
|
||||
)
|
||||
(func "$(lib)/allocator/arena/reset_memory" (; 2 ;) (type $v)
|
||||
;;@ (lib)/allocator/arena.ts:40:2
|
||||
(set_global "$(lib)/allocator/arena/OFFSET"
|
||||
;;@ (lib)/allocator/arena.ts:40:11
|
||||
(i32.and
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:40:12
|
||||
(get_global $HEAP_BASE)
|
||||
;;@ (lib)/allocator/arena.ts:40:24
|
||||
(i32.const 7)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:40:35
|
||||
(i32.xor
|
||||
;;@ (lib)/allocator/arena.ts:40:36
|
||||
(i32.const 7)
|
||||
(i32.const -1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(func $start (; 3 ;) (type $v)
|
||||
(set_global "$(lib)/allocator/arena/OFFSET"
|
||||
;;@ (lib)/allocator/arena.ts:12:20
|
||||
(i32.and
|
||||
(i32.add
|
||||
;;@ (lib)/allocator/arena.ts:12:21
|
||||
(get_global $HEAP_BASE)
|
||||
;;@ (lib)/allocator/arena.ts:12:33
|
||||
(i32.const 7)
|
||||
)
|
||||
;;@ (lib)/allocator/arena.ts:12:44
|
||||
(i32.xor
|
||||
;;@ (lib)/allocator/arena.ts:12:45
|
||||
(i32.const 7)
|
||||
(i32.const -1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
@ -1,541 +0,0 @@
|
||||
/**
|
||||
* @file Buddy Memory Allocator
|
||||
*/
|
||||
|
||||
/*
|
||||
Copyright 2018 Evan Wallace
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/// see: https://github.com/evanw/buddy-malloc
|
||||
|
||||
/*
|
||||
* This file implements a buddy memory allocator, which is an allocator that
|
||||
* allocates memory within a fixed linear address range. It spans the address
|
||||
* range with a binary tree that tracks free space. Both "malloc" and "free"
|
||||
* are O(log N) time where N is the maximum possible number of allocations.
|
||||
*
|
||||
* The "buddy" term comes from how the tree is used. When memory is allocated,
|
||||
* nodes in the tree are split recursively until a node of the appropriate size
|
||||
* is reached. Every split results in two child nodes, each of which is the
|
||||
* buddy of the other. When a node is freed, the node and its buddy can be
|
||||
* merged again if the buddy is also free. This makes the memory available
|
||||
* for larger allocations again.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Every allocation needs an 8-byte header to store the allocation size while
|
||||
* staying 8-byte aligned. The address returned by "malloc" is the address
|
||||
* right after this header (i.e. the size occupies the 8 bytes before the
|
||||
* returned address).
|
||||
*/
|
||||
const HEADER_SIZE: usize = 8;
|
||||
|
||||
/*
|
||||
* The minimum allocation size is 16 bytes because we have an 8-byte header and
|
||||
* we need to stay 8-byte aligned.
|
||||
*/
|
||||
const MIN_ALLOC_LOG2: usize = 4;
|
||||
const MIN_ALLOC: usize = 1 << MIN_ALLOC_LOG2;
|
||||
|
||||
/*
|
||||
* The maximum allocation size is currently set to 2gb. This is the total size
|
||||
* of the heap. It's technically also the maximum allocation size because the
|
||||
* heap could consist of a single allocation of this size. But of course real
|
||||
* heaps will have multiple allocations, so the real maximum allocation limit
|
||||
* is at most 1gb.
|
||||
*/
|
||||
const MAX_ALLOC_LOG2: usize = 31;
|
||||
const MAX_ALLOC: usize = 1 << MAX_ALLOC_LOG2;
|
||||
|
||||
/*
|
||||
* Allocations are done in powers of two starting from MIN_ALLOC and ending at
|
||||
* MAX_ALLOC inclusive. Each allocation size has a bucket that stores the free
|
||||
* list for that allocation size.
|
||||
*
|
||||
* Given a bucket index, the size of the allocations in that bucket can be
|
||||
* found with "(size_t)1 << (MAX_ALLOC_LOG2 - bucket)".
|
||||
*/
|
||||
const BUCKET_COUNT: usize = MAX_ALLOC_LOG2 - MIN_ALLOC_LOG2 + 1;
|
||||
|
||||
/*
|
||||
* Free lists are stored as circular doubly-linked lists. Every possible
|
||||
* allocation size has an associated free list that is threaded through all
|
||||
* currently free blocks of that size. That means MIN_ALLOC must be at least
|
||||
* "sizeof(list_t)". MIN_ALLOC is currently 16 bytes, so this will be true for
|
||||
* both 32-bit and 64-bit.
|
||||
*/
|
||||
@unmanaged
|
||||
class List {
|
||||
prev: List;
|
||||
next: List;
|
||||
static readonly SIZE: usize = 2 * sizeof<usize>();
|
||||
}
|
||||
|
||||
/*
|
||||
* Each bucket corresponds to a certain allocation size and stores a free list
|
||||
* for that size. The bucket at index 0 corresponds to an allocation size of
|
||||
* MAX_ALLOC (i.e. the whole address space).
|
||||
*/
|
||||
var BUCKETS_START: usize = HEAP_BASE;
|
||||
var BUCKETS_END: usize = BUCKETS_START + BUCKET_COUNT * List.SIZE;
|
||||
|
||||
function buckets$get(index: usize): List {
|
||||
assert(index < BUCKET_COUNT);
|
||||
return changetype<List>(BUCKETS_START + index * List.SIZE);
|
||||
}
|
||||
|
||||
/*
|
||||
* We could initialize the allocator by giving it one free block the size of
|
||||
* the entire address space. However, this would cause us to instantly reserve
|
||||
* half of the entire address space on the first allocation, since the first
|
||||
* split would store a free list entry at the start of the right child of the
|
||||
* root. Instead, we have the tree start out small and grow the size of the
|
||||
* tree as we use more memory. The size of the tree is tracked by this value.
|
||||
*/
|
||||
var bucket_limit: usize;
|
||||
|
||||
/*
|
||||
* This array represents a linearized binary tree of bits. Every possible
|
||||
* allocation larger than MIN_ALLOC has a node in this tree (and therefore a
|
||||
* bit in this array).
|
||||
*
|
||||
* Given the index for a node, lineraized binary trees allow you to traverse to
|
||||
* the parent node or the child nodes just by doing simple arithmetic on the
|
||||
* index:
|
||||
*
|
||||
* - Move to parent: index = (index - 1) / 2;
|
||||
* - Move to left child: index = index * 2 + 1;
|
||||
* - Move to right child: index = index * 2 + 2;
|
||||
* - Move to sibling: index = ((index - 1) ^ 1) + 1;
|
||||
*
|
||||
* Each node in this tree can be in one of several states:
|
||||
*
|
||||
* - UNUSED (both children are UNUSED)
|
||||
* - SPLIT (one child is UNUSED and the other child isn't)
|
||||
* - USED (neither children are UNUSED)
|
||||
*
|
||||
* These states take two bits to store. However, it turns out we have enough
|
||||
* information to distinguish between UNUSED and USED from context, so we only
|
||||
* need to store SPLIT or not, which only takes a single bit.
|
||||
*
|
||||
* Note that we don't need to store any nodes for allocations of size MIN_ALLOC
|
||||
* since we only ever care about parent nodes.
|
||||
*/
|
||||
const SPLIT_COUNT: usize = (1 << (BUCKET_COUNT - 1)) / 8;
|
||||
var NODE_IS_SPLIT_START: usize = BUCKETS_END;
|
||||
var NODE_IS_SPLIT_END: usize = NODE_IS_SPLIT_START + SPLIT_COUNT * sizeof<u8>();
|
||||
|
||||
function node_is_split$get(index: usize): i32 {
|
||||
assert(index < SPLIT_COUNT);
|
||||
return load<u8>(NODE_IS_SPLIT_START + index);
|
||||
}
|
||||
|
||||
function node_is_split$set(index: usize, state: i32): void {
|
||||
assert(index < SPLIT_COUNT);
|
||||
store<u8>(NODE_IS_SPLIT_START + index, state);
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the starting address of the address range for this allocator. Every
|
||||
* returned allocation will be an offset of this pointer from 0 to MAX_ALLOC.
|
||||
*/
|
||||
var base_ptr: usize;
|
||||
|
||||
/*
|
||||
* This is the maximum address that has ever been used by the allocator. It's
|
||||
* used to know when to call "brk" to request more memory from the kernel.
|
||||
*/
|
||||
var max_ptr: usize;
|
||||
|
||||
/*
|
||||
* Make sure all addresses before "new_value" are valid and can be used. Memory
|
||||
* is allocated in a 2gb address range but that memory is not reserved up
|
||||
* front. It's only reserved when it's needed by calling this function. This
|
||||
* will return false if the memory could not be reserved.
|
||||
*/
|
||||
function update_max_ptr(new_value: usize): i32 {
|
||||
if (new_value > max_ptr) {
|
||||
// if (brk(new_value)) {
|
||||
// return 0;
|
||||
// }
|
||||
var oldPages = <u32>current_memory();
|
||||
var newPages = <u32>(((new_value + 0xffff) & ~0xffff) >> 16);
|
||||
assert(newPages > oldPages);
|
||||
if (grow_memory(newPages - oldPages) < 0) {
|
||||
return 0;
|
||||
}
|
||||
// max_ptr = new_value;
|
||||
max_ptr = <usize>newPages << 16;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize a list to empty. Because these are circular lists, an "empty"
|
||||
* list is an entry where both links point to itself. This makes insertion
|
||||
* and removal simpler because they don't need any branches.
|
||||
*/
|
||||
function list_init(list: List): void {
|
||||
list.prev = list;
|
||||
list.next = list;
|
||||
}
|
||||
|
||||
/*
|
||||
* Append the provided entry to the end of the list. This assumes the entry
|
||||
* isn't in a list already because it overwrites the linked list pointers.
|
||||
*/
|
||||
function list_push(list: List, entry: List): void {
|
||||
var prev = list.prev;
|
||||
entry.prev = prev;
|
||||
entry.next = list;
|
||||
prev.next = entry;
|
||||
list.prev = entry;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove the provided entry from whichever list it's currently in. This
|
||||
* assumes that the entry is in a list. You don't need to provide the list
|
||||
* because the lists are circular, so the list's pointers will automatically
|
||||
* be updated if the first or last entries are removed.
|
||||
*/
|
||||
function list_remove(entry: List): void {
|
||||
var prev = entry.prev;
|
||||
var next = entry.next;
|
||||
prev.next = next;
|
||||
next.prev = prev;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove and return the first entry in the list or NULL if the list is empty.
|
||||
*/
|
||||
function list_pop(list: List): List | null {
|
||||
var back = list.prev;
|
||||
if (back == list) return null;
|
||||
list_remove(back);
|
||||
return back;
|
||||
}
|
||||
|
||||
/*
|
||||
* This maps from the index of a node to the address of memory that node
|
||||
* represents. The bucket can be derived from the index using a loop but is
|
||||
* required to be provided here since having them means we can avoid the loop
|
||||
* and have this function return in constant time.
|
||||
*/
|
||||
function ptr_for_node(index: usize, bucket: usize): usize {
|
||||
return base_ptr + ((index - (1 << bucket) + 1) << (MAX_ALLOC_LOG2 - bucket));
|
||||
}
|
||||
|
||||
/*
|
||||
* This maps from an address of memory to the node that represents that
|
||||
* address. There are often many nodes that all map to the same address, so
|
||||
* the bucket is needed to uniquely identify a node.
|
||||
*/
|
||||
function node_for_ptr(ptr: usize, bucket: usize): usize {
|
||||
return ((ptr - base_ptr) >> (MAX_ALLOC_LOG2 - bucket)) + (1 << bucket) - 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Given the index of a node, this returns the "is split" flag of the parent.
|
||||
*/
|
||||
function parent_is_split(index: usize): i32 {
|
||||
index = (index - 1) / 2;
|
||||
return (node_is_split$get(index / 8) >>> <i32>(index % 8)) & 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Given the index of a node, this flips the "is split" flag of the parent.
|
||||
*/
|
||||
function flip_parent_is_split(index: usize): void {
|
||||
index = (index - 1) / 2;
|
||||
var indexDiv8 = index / 8;
|
||||
node_is_split$set(indexDiv8,
|
||||
node_is_split$get(indexDiv8) ^ <i32>(1 << (index % 8))
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Given the requested size passed to "malloc", this function returns the index
|
||||
* of the smallest bucket that can fit that size.
|
||||
*/
|
||||
function bucket_for_request(request: usize): usize {
|
||||
var bucket = BUCKET_COUNT - 1;
|
||||
var size = MIN_ALLOC;
|
||||
|
||||
while (size < request) {
|
||||
bucket--;
|
||||
size *= 2;
|
||||
}
|
||||
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/*
|
||||
* The tree is always rooted at the current bucket limit. This call grows the
|
||||
* tree by repeatedly doubling it in size until the root lies at the provided
|
||||
* bucket index. Each doubling lowers the bucket limit by 1.
|
||||
*/
|
||||
function lower_bucket_limit(bucket: usize): u32 {
|
||||
while (bucket < bucket_limit) {
|
||||
var root = node_for_ptr(base_ptr, bucket_limit);
|
||||
var right_child: usize;
|
||||
|
||||
/*
|
||||
* If the parent isn't SPLIT, that means the node at the current bucket
|
||||
* limit is UNUSED and our address space is entirely free. In that case,
|
||||
* clear the root free list, increase the bucket limit, and add a single
|
||||
* block with the newly-expanded address space to the new root free list.
|
||||
*/
|
||||
if (!parent_is_split(root)) {
|
||||
list_remove(changetype<List>(base_ptr));
|
||||
list_init(buckets$get(--bucket_limit));
|
||||
list_push(buckets$get(bucket_limit), changetype<List>(base_ptr));
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Otherwise, the tree is currently in use. Create a parent node for the
|
||||
* current root node in the SPLIT state with a right child on the free
|
||||
* list. Make sure to reserve the memory for the free list entry before
|
||||
* writing to it. Note that we do not need to flip the "is split" flag for
|
||||
* our current parent because it's already on (we know because we just
|
||||
* checked it above).
|
||||
*/
|
||||
right_child = ptr_for_node(root + 1, bucket_limit);
|
||||
if (!update_max_ptr(right_child + List.SIZE)) {
|
||||
return 0;
|
||||
}
|
||||
list_push(buckets$get(bucket_limit), changetype<List>(right_child));
|
||||
list_init(buckets$get(--bucket_limit));
|
||||
|
||||
/*
|
||||
* Set the grandparent's SPLIT flag so if we need to lower the bucket limit
|
||||
* again, we'll know that the new root node we just added is in use.
|
||||
*/
|
||||
root = (root - 1) / 2;
|
||||
if (root != 0) {
|
||||
flip_parent_is_split(root);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@global
|
||||
function allocate_memory(request: usize): usize {
|
||||
var original_bucket: usize, bucket: usize;
|
||||
|
||||
/*
|
||||
* Make sure it's possible for an allocation of this size to succeed. There's
|
||||
* a hard-coded limit on the maximum allocation size because of the way this
|
||||
* allocator works.
|
||||
*/
|
||||
if (request + HEADER_SIZE > MAX_ALLOC) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize our global state if this is the first call to "malloc". At the
|
||||
* beginning, the tree has a single node that represents the smallest
|
||||
* possible allocation size. More memory will be reserved later as needed.
|
||||
*/
|
||||
if (base_ptr == 0) {
|
||||
// base_ptr = max_ptr = (uint8_t *)sbrk(0);
|
||||
base_ptr = (NODE_IS_SPLIT_END + 7) & ~7; // must be aligned
|
||||
max_ptr = <usize>current_memory() << 16; // must grow first
|
||||
bucket_limit = BUCKET_COUNT - 1;
|
||||
if (!update_max_ptr(base_ptr + List.SIZE)) {
|
||||
return 0;
|
||||
}
|
||||
list_init(buckets$get(BUCKET_COUNT - 1));
|
||||
list_push(buckets$get(BUCKET_COUNT - 1), changetype<List>(base_ptr));
|
||||
}
|
||||
|
||||
/*
|
||||
* Find the smallest bucket that will fit this request. This doesn't check
|
||||
* that there's space for the request yet.
|
||||
*/
|
||||
bucket = bucket_for_request(request + HEADER_SIZE);
|
||||
original_bucket = bucket;
|
||||
|
||||
/*
|
||||
* Search for a bucket with a non-empty free list that's as large or larger
|
||||
* than what we need. If there isn't an exact match, we'll need to split a
|
||||
* larger one to get a match.
|
||||
*/
|
||||
while (bucket + 1 != 0) {
|
||||
var size: usize, bytes_needed: usize, i: usize;
|
||||
var ptr: usize;
|
||||
|
||||
/*
|
||||
* We may need to grow the tree to be able to fit an allocation of this
|
||||
* size. Try to grow the tree and stop here if we can't.
|
||||
*/
|
||||
if (!lower_bucket_limit(bucket)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Try to pop a block off the free list for this bucket. If the free list
|
||||
* is empty, we're going to have to split a larger block instead.
|
||||
*/
|
||||
ptr = changetype<usize>(list_pop(buckets$get(bucket)));
|
||||
if (!ptr) {
|
||||
/*
|
||||
* If we're not at the root of the tree or it's impossible to grow the
|
||||
* tree any more, continue on to the next bucket.
|
||||
*/
|
||||
if (bucket != bucket_limit || bucket == 0) {
|
||||
bucket--;
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Otherwise, grow the tree one more level and then pop a block off the
|
||||
* free list again. Since we know the root of the tree is used (because
|
||||
* the free list was empty), this will add a parent above this node in
|
||||
* the SPLIT state and then add the new right child node to the free list
|
||||
* for this bucket. Popping the free list will give us this right child.
|
||||
*/
|
||||
if (!lower_bucket_limit(bucket - 1)) {
|
||||
return 0;
|
||||
}
|
||||
ptr = changetype<usize>(list_pop(buckets$get(bucket)));
|
||||
}
|
||||
|
||||
/*
|
||||
* Try to expand the address space first before going any further. If we
|
||||
* have run out of space, put this block back on the free list and fail.
|
||||
*/
|
||||
size = 1 << (MAX_ALLOC_LOG2 - bucket);
|
||||
bytes_needed = bucket < original_bucket ? size / 2 + List.SIZE : size;
|
||||
if (!update_max_ptr(ptr + bytes_needed)) {
|
||||
list_push(buckets$get(bucket), changetype<List>(ptr));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we got a node off the free list, change the node from UNUSED to USED.
|
||||
* This involves flipping our parent's "is split" bit because that bit is
|
||||
* the exclusive-or of the UNUSED flags of both children, and our UNUSED
|
||||
* flag (which isn't ever stored explicitly) has just changed.
|
||||
*
|
||||
* Note that we shouldn't ever need to flip the "is split" bit of our
|
||||
* grandparent because we know our buddy is USED so it's impossible for our
|
||||
* grandparent to be UNUSED (if our buddy chunk was UNUSED, our parent
|
||||
* wouldn't ever have been split in the first place).
|
||||
*/
|
||||
i = node_for_ptr(ptr, bucket);
|
||||
if (i != 0) {
|
||||
flip_parent_is_split(i);
|
||||
}
|
||||
|
||||
/*
|
||||
* If the node we got is larger than we need, split it down to the correct
|
||||
* size and put the new unused child nodes on the free list in the
|
||||
* corresponding bucket. This is done by repeatedly moving to the left
|
||||
* child, splitting the parent, and then adding the right child to the free
|
||||
* list.
|
||||
*/
|
||||
while (bucket < original_bucket) {
|
||||
i = i * 2 + 1;
|
||||
bucket++;
|
||||
flip_parent_is_split(i);
|
||||
list_push(
|
||||
buckets$get(bucket),
|
||||
changetype<List>(ptr_for_node(i + 1, bucket))
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Now that we have a memory address, write the block header (just the size
|
||||
* of the allocation) and return the address immediately after the header.
|
||||
*/
|
||||
store<usize>(ptr, request);
|
||||
return ptr + HEADER_SIZE;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@global
|
||||
function free_memory(ptr: usize): void {
|
||||
var bucket: usize, i: usize;
|
||||
|
||||
/*
|
||||
* Ignore any attempts to free a NULL pointer.
|
||||
*/
|
||||
if (!ptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* We were given the address returned by "malloc" so get back to the actual
|
||||
* address of the node by subtracting off the size of the block header. Then
|
||||
* look up the index of the node corresponding to this address.
|
||||
*/
|
||||
ptr = ptr - HEADER_SIZE;
|
||||
bucket = bucket_for_request(load<usize>(ptr) + HEADER_SIZE);
|
||||
i = node_for_ptr(ptr, bucket);
|
||||
|
||||
/*
|
||||
* Traverse up to the root node, flipping USED blocks to UNUSED and merging
|
||||
* UNUSED buddies together into a single UNUSED parent.
|
||||
*/
|
||||
while (i != 0) {
|
||||
/*
|
||||
* Change this node from UNUSED to USED. This involves flipping our
|
||||
* parent's "is split" bit because that bit is the exclusive-or of the
|
||||
* UNUSED flags of both children, and our UNUSED flag (which isn't ever
|
||||
* stored explicitly) has just changed.
|
||||
*/
|
||||
flip_parent_is_split(i);
|
||||
|
||||
/*
|
||||
* If the parent is now SPLIT, that means our buddy is USED, so don't merge
|
||||
* with it. Instead, stop the iteration here and add ourselves to the free
|
||||
* list for our bucket.
|
||||
*
|
||||
* Also stop here if we're at the current root node, even if that root node
|
||||
* is now UNUSED. Root nodes don't have a buddy so we can't merge with one.
|
||||
*/
|
||||
if (parent_is_split(i) || bucket == bucket_limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we get here, we know our buddy is UNUSED. In this case we should
|
||||
* merge with that buddy and continue traversing up to the root node. We
|
||||
* need to remove the buddy from its free list here but we don't need to
|
||||
* add the merged parent to its free list yet. That will be done once after
|
||||
* this loop is finished.
|
||||
*/
|
||||
list_remove(changetype<List>(ptr_for_node(((i - 1) ^ 1) + 1, bucket)));
|
||||
i = (i - 1) / 2;
|
||||
bucket--;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add ourselves to the free list for our bucket. We add to the back of the
|
||||
* list because "malloc" takes from the back of the list and we want a "free"
|
||||
* followed by a "malloc" of the same size to ideally use the same address
|
||||
* for better memory locality.
|
||||
*/
|
||||
list_push(buckets$get(bucket), changetype<List>(ptr_for_node(i, bucket)));
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
import "./buddy";
|
||||
import "allocator/buddy";
|
||||
export { allocate_memory, free_memory };
|
||||
// export { set_memory };
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1110
tests/allocators/buddy/optimized.wat
Normal file
1110
tests/allocators/buddy/optimized.wat
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run build:untouched && npm run build:optimized",
|
||||
"build:untouched": "asc assembly/index.ts -t buddy.untouched.wat -b buddy.untouched.wasm --validate --sourceMap --measure",
|
||||
"build:optimized": "asc assembly/index.ts -t buddy.optimized.wat -b buddy.optimized.wasm --validate --sourceMap --measure --noDebug --noAssert --optimize",
|
||||
"test": "node ./index",
|
||||
"test:forever": "node ../forever buddy"
|
||||
"build:untouched": "asc assembly/index.ts -t untouched.wat -b untouched.wasm --validate --sourceMap --measure",
|
||||
"build:optimized": "asc assembly/index.ts -t optimized.wat -b optimized.wasm --validate --sourceMap --measure --noDebug --noAssert --optimize"
|
||||
}
|
||||
}
|
||||
|
1363
tests/allocators/buddy/untouched.wat
Normal file
1363
tests/allocators/buddy/untouched.wat
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,13 +2,15 @@ var child_process = require("child_process");
|
||||
|
||||
// restarts the test forever, that is, until an issue is detected
|
||||
|
||||
if (process.argv.length < 2) {
|
||||
console.error("Usage: npm run test:forever <allocator>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
while (true) {
|
||||
console.log("[ #" + ++count + " ]\n");
|
||||
var script = process.argv.length > 2
|
||||
? __dirname + "/" + process.argv[2]
|
||||
: __dirname;
|
||||
var res = child_process.spawnSync("node", [ script ], { stdio: "inherit" });
|
||||
var res = child_process.spawnSync("node", [ "./index", process.argv[2] ], { stdio: "inherit" });
|
||||
if (res.status !== 0)
|
||||
throw Error("exited with " + res.status);
|
||||
if (res.error)
|
||||
|
@ -19,11 +19,16 @@ function test(file) {
|
||||
return String.fromCharCode.apply(String, str);
|
||||
}
|
||||
|
||||
require("../runner")(exports, 50, 20000);
|
||||
require("./runner")(exports, 50, 20000);
|
||||
|
||||
console.log("mem final: " + exports.memory.buffer.byteLength);
|
||||
console.log();
|
||||
}
|
||||
|
||||
test("buddy.untouched.wasm");
|
||||
test("buddy.optimized.wasm");
|
||||
if (process.argv.length > 2) {
|
||||
test(process.argv[2] + "/untouched.wasm");
|
||||
test(process.argv[2] + "/optimized.wasm");
|
||||
} else {
|
||||
console.error("Usage: npm test <allocator>");
|
||||
process.exit(1);
|
||||
}
|
7
tests/allocators/package.json
Normal file
7
tests/allocators/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "node ./index",
|
||||
"test:forever": "node ./forever"
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
function runner(allocator, runs, allocs) {
|
||||
var hasReset = !!allocator.reset_memory;
|
||||
var useSet = !!allocator.set_memory;
|
||||
console.log("hasReset=" + hasReset + ", useSet=" + useSet);
|
||||
var ptrs = [];
|
||||
|
||||
function randomAlloc(maxSize) {
|
||||
@ -9,7 +12,7 @@ function runner(allocator, runs, allocs) {
|
||||
if (!ptr) throw Error();
|
||||
if ((ptr & 7) != 0) throw Error("invalid alignment: " + (ptr & 7) + " on " + ptr);
|
||||
if (ptrs.indexOf(ptr) >= 0) throw Error("duplicate pointer");
|
||||
if (allocator.set_memory)
|
||||
if (useSet)
|
||||
allocator.set_memory(ptr, 0xdc, size);
|
||||
ptrs.push(ptr);
|
||||
return ptr;
|
||||
@ -35,7 +38,11 @@ function runner(allocator, runs, allocs) {
|
||||
// remember the smallest possible memory address
|
||||
var base = allocator.allocate_memory(64);
|
||||
console.log("base: " + base);
|
||||
allocator.free_memory(base);
|
||||
if (hasReset) {
|
||||
allocator.reset_memory();
|
||||
} else {
|
||||
allocator.free_memory(base);
|
||||
}
|
||||
var currentMem = allocator.memory.buffer.byteLength;
|
||||
console.log("mem initial: " + currentMem);
|
||||
|
||||
@ -65,16 +72,24 @@ function runner(allocator, runs, allocs) {
|
||||
// free the rest, randomly
|
||||
while (ptrs.length) randomFree();
|
||||
|
||||
// should now be possible to reuse the entire memory
|
||||
// just try a large portion of the memory here, for example because of
|
||||
// SL+1 for allocations in TLSF
|
||||
var size = ((allocator.memory.buffer.byteLength - base) * 9 / 10) >>> 0;
|
||||
var ptr = allocator.allocate_memory(size);
|
||||
if (allocator.set_memory)
|
||||
allocator.set_memory(ptr, 0xac, size);
|
||||
if (ptr !== base)
|
||||
throw Error("expected " + base + " but got " + ptr);
|
||||
allocator.free_memory(ptr);
|
||||
if (hasReset) {
|
||||
allocator.reset_memory();
|
||||
var ptr = allocator.allocate_memory(64);
|
||||
if (ptr !== base)
|
||||
throw Error("expected " + base + " but got " + ptr);
|
||||
allocator.reset_memory();
|
||||
} else {
|
||||
// should now be possible to reuse the entire memory
|
||||
// just try a large portion of the memory here, for example because of
|
||||
// SL+1 for allocations in TLSF
|
||||
var size = ((allocator.memory.buffer.byteLength - base) * 9 / 10) >>> 0;
|
||||
var ptr = allocator.allocate_memory(size);
|
||||
if (useSet)
|
||||
allocator.set_memory(ptr, 0xac, size);
|
||||
if (ptr !== base)
|
||||
throw Error("expected " + base + " but got " + ptr);
|
||||
allocator.free_memory(ptr);
|
||||
}
|
||||
testMemChanged();
|
||||
}
|
||||
} finally {
|
||||
|
@ -1,28 +0,0 @@
|
||||
const fs = require("fs");
|
||||
|
||||
function test(file) {
|
||||
console.log("Testing '" + file + "' ...\n");
|
||||
|
||||
const exports = new WebAssembly.Instance(WebAssembly.Module(fs.readFileSync(__dirname + "/" + file)), {
|
||||
env: {
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
require("../runner")(exports, 50, 20000); // picked so I/O isn't the bottleneck
|
||||
console.log("mem final: " + exports.memory.buffer.byteLength);
|
||||
console.log();
|
||||
}
|
||||
|
||||
test("tlsf.untouched.wasm");
|
||||
test("tlsf.optimized.wasm");
|
@ -2,9 +2,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run build:untouched && npm run build:optimized",
|
||||
"build:untouched": "asc assembly/index.ts -t tlsf.untouched.wat -b tlsf.untouched.wasm --validate --sourceMap --measure",
|
||||
"build:optimized": "asc assembly/index.ts -t tlsf.optimized.wat -b tlsf.optimized.wasm --validate --sourceMap --measure --noDebug --noAssert --optimize",
|
||||
"test": "node ./index",
|
||||
"test:forever": "node ../forever tlsf"
|
||||
"build:untouched": "asc assembly/index.ts -t untouched.wat -b untouched.wasm --validate --sourceMap --measure",
|
||||
"build:optimized": "asc assembly/index.ts -t optimized.wat -b optimized.wasm --validate --sourceMap --measure --noDebug --noAssert --optimize"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user