diff --git a/scripts/trace.sh b/scripts/trace.sh new file mode 100644 index 00000000000000..32655bc5267688 --- /dev/null +++ b/scripts/trace.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# @file trace.sh +# @summary build + run bun with Instruments. All args are forwarded to `bun-debug`. +# +# @description +# This script builds bun, signs it with debug entitlements, and runs it with an +# Allocations template. After running, a `.trace` folder will be created. Open +# it with `open foo.trace` to view it in Instruments. +# +# This script requires xcode command line tools to be installed and only works +# on MacOS. + +set -e -o pipefail + +BUN="bun-debug" +DEBUG_BUN="build/debug/${BUN}" + +file_to_run=$1 +if [[ -z $file_to_run ]]; then + echo "Usage: $0 [bun args]" + echo " $0 test [bun args]" + exit 1 +fi + +bun run build + +echo "Signing bun binary..." +codesign --entitlements $(realpath entitlements.debug.plist) --force --timestamp --sign - -vvvv --deep --strict ${DEBUG_BUN} + +export BUN_JSC_logJITCodeForPerf=1 +export BUN_JSC_collectExtraSamplingProfilerData=1 +export BUN_JSC_sampleCCode=1 +export BUN_JSC_alwaysGeneratePCToCodeOriginMap=1 + +echo "Tracing ${file_to_run}..." +xcrun xctrace record --template "Allocations" -output . --launch -- "./${DEBUG_BUN}" $file_to_run +# perf record -k 1 --sample-cpu -e cycles:u -j any --call-graph dwarf,16384 -F 499 -p (pgrep -f "${BUN}") + +# DEBUGINFOD_URLS="" perf inject --jit --input perf.data --output=perf.jit.data -v diff --git a/src/NullableAllocator.zig b/src/allocators/NullableAllocator.zig similarity index 99% rename from src/NullableAllocator.zig rename to src/allocators/NullableAllocator.zig index 289eb98f7d7747..ccbf9725796e3d 100644 --- a/src/NullableAllocator.zig +++ b/src/allocators/NullableAllocator.zig @@ -1,8 +1,9 @@ //! A nullable allocator the same size as `std.mem.Allocator`. const std = @import("std"); -const NullableAllocator = @This(); const bun = @import("root").bun; +const NullableAllocator = @This(); + ptr: *anyopaque = undefined, // Utilize the null pointer optimization on the vtable instead of // the regular ptr because some allocator implementations might tag their diff --git a/src/linux_memfd_allocator.zig b/src/allocators/linux_memfd_allocator.zig similarity index 93% rename from src/linux_memfd_allocator.zig rename to src/allocators/linux_memfd_allocator.zig index c2ec4f7f07cdae..0206fd9398ef60 100644 --- a/src/linux_memfd_allocator.zig +++ b/src/allocators/linux_memfd_allocator.zig @@ -31,9 +31,18 @@ pub const LinuxMemFdAllocator = struct { } pub fn deref(this: *LinuxMemFdAllocator) void { - if (this.ref_count.fetchSub(1, .monotonic) == 1) { - _ = bun.sys.close(this.fd); - this.destroy(); + switch (this.ref_count.fetchSub(1, .monotonic)) { + 1 => { + _ = bun.sys.close(this.fd); + this.destroy(); + }, + 0 => { + // TODO: @branchHint(.cold) after Zig 0.14 upgrade + if (comptime bun.Environment.isDebug) { + std.debug.panic("LinuxMemFdAllocator ref_count underflow", .{}); + } + }, + else => {}, } } diff --git a/src/max_heap_allocator.zig b/src/allocators/max_heap_allocator.zig similarity index 100% rename from src/max_heap_allocator.zig rename to src/allocators/max_heap_allocator.zig diff --git a/src/memory_allocator.zig b/src/allocators/memory_allocator.zig similarity index 97% rename from src/memory_allocator.zig rename to src/allocators/memory_allocator.zig index 18f7e651cce2b9..07b84ef7b3476f 100644 --- a/src/memory_allocator.zig +++ b/src/allocators/memory_allocator.zig @@ -5,9 +5,9 @@ const bun = @import("root").bun; const log = bun.Output.scoped(.mimalloc, true); const assert = bun.assert; const Allocator = mem.Allocator; -const mimalloc = @import("./allocators/mimalloc.zig"); -const FeatureFlags = @import("./feature_flags.zig"); -const Environment = @import("./env.zig"); +const mimalloc = @import("./mimalloc.zig"); +const FeatureFlags = @import("../feature_flags.zig"); +const Environment = @import("../env.zig"); fn mimalloc_free( _: *anyopaque, diff --git a/src/mimalloc_arena.zig b/src/allocators/mimalloc_arena.zig similarity index 98% rename from src/mimalloc_arena.zig rename to src/allocators/mimalloc_arena.zig index d44ba21b765dab..48d0a30acc5d9b 100644 --- a/src/mimalloc_arena.zig +++ b/src/allocators/mimalloc_arena.zig @@ -2,9 +2,9 @@ const mem = @import("std").mem; const builtin = @import("std").builtin; const std = @import("std"); -const mimalloc = @import("./allocators/mimalloc.zig"); -const Environment = @import("./env.zig"); -const FeatureFlags = @import("./feature_flags.zig"); +const mimalloc = @import("./mimalloc.zig"); +const Environment = @import("../env.zig"); +const FeatureFlags = @import("../feature_flags.zig"); const Allocator = mem.Allocator; const assert = bun.assert; const bun = @import("root").bun; diff --git a/src/bake/DevServer.zig b/src/bake/DevServer.zig index 5f59b524011936..09c6cbf9971d2a 100644 --- a/src/bake/DevServer.zig +++ b/src/bake/DevServer.zig @@ -4485,5 +4485,5 @@ const JSModuleLoader = JSC.JSModuleLoader; const EventLoopHandle = JSC.EventLoopHandle; const JSInternalPromise = JSC.JSInternalPromise; -const ThreadlocalArena = @import("../mimalloc_arena.zig").Arena; +const ThreadlocalArena = @import("../allocators/mimalloc_arena.zig").Arena; const Chunk = bun.bundle_v2.Chunk; diff --git a/src/bun.js/api/JSBundler.zig b/src/bun.js/api/JSBundler.zig index a75e260f72ced4..75efdc43c3f38d 100644 --- a/src/bun.js/api/JSBundler.zig +++ b/src/bun.js/api/JSBundler.zig @@ -38,7 +38,7 @@ const JSAst = bun.JSAst; const JSParser = bun.js_parser; const JSPrinter = bun.js_printer; const ScanPassResult = JSParser.ScanPassResult; -const Mimalloc = @import("../../mimalloc_arena.zig"); +const Mimalloc = @import("../../allocators/mimalloc_arena.zig"); const Runtime = @import("../../runtime.zig").Runtime; const JSLexer = bun.js_lexer; const Expr = JSAst.Expr; diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index 77d642b3f94652..0a2462cc77758b 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -36,7 +36,7 @@ const JSAst = bun.JSAst; const JSParser = bun.js_parser; const JSPrinter = bun.js_printer; const ScanPassResult = JSParser.ScanPassResult; -const Mimalloc = @import("../../mimalloc_arena.zig"); +const Mimalloc = @import("../../allocators/mimalloc_arena.zig"); const Runtime = @import("../../runtime.zig").Runtime; const JSLexer = bun.js_lexer; const Expr = JSAst.Expr; diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index c2ab4bcfb0378d..686bdaf49f67ad 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -75,7 +75,7 @@ const Fallback = Runtime.Fallback; const MimeType = HTTP.MimeType; const Blob = JSC.WebCore.Blob; const BoringSSL = bun.BoringSSL; -const Arena = @import("../../mimalloc_arena.zig").Arena; +const Arena = @import("../../allocators/mimalloc_arena.zig").Arena; const SendfileContext = struct { fd: bun.FileDescriptor, socket_fd: bun.FileDescriptor = bun.invalid_fd, diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index e3f0b6f5a5d7df..57bd18ade7b168 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -12,7 +12,7 @@ const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const StoredFileDescriptorType = bun.StoredFileDescriptorType; const ErrorableString = bun.JSC.ErrorableString; -const Arena = @import("../mimalloc_arena.zig").Arena; +const Arena = @import("../allocators/mimalloc_arena.zig").Arena; const C = bun.C; const Exception = bun.JSC.Exception; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 8b7103acf1cfe1..db1272731814ec 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -11,7 +11,7 @@ const MutableString = bun.MutableString; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const StoredFileDescriptorType = bun.StoredFileDescriptorType; -const Arena = @import("../mimalloc_arena.zig").Arena; +const Arena = @import("../allocators/mimalloc_arena.zig").Arena; const C = bun.C; const Allocator = std.mem.Allocator; diff --git a/src/bun.zig b/src/bun.zig index 6bb9b890a5d5c4..0dd0dfdfad3d14 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -16,23 +16,23 @@ pub const use_mimalloc = true; pub const default_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").c_allocator; + @import("./allocators/memory_allocator.zig").c_allocator; /// Zeroing memory allocator pub const z_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").z_allocator; + @import("./allocators/memory_allocator.zig").z_allocator; pub const huge_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").huge_allocator; + @import("./allocators/memory_allocator.zig").huge_allocator; pub const auto_allocator: std.mem.Allocator = if (!use_mimalloc) std.heap.c_allocator else - @import("./memory_allocator.zig").auto_allocator; + @import("./allocators/memory_allocator.zig").auto_allocator; pub const callmod_inline: std.builtin.CallModifier = if (builtin.mode == .Debug) .auto else .always_inline; pub const callconv_inline: std.builtin.CallingConvention = if (builtin.mode == .Debug) .Unspecified else .Inline; @@ -556,7 +556,7 @@ pub const StringBuilder = @import("./string_builder.zig"); pub const LinearFifo = @import("./linear_fifo.zig").LinearFifo; pub const linux = struct { - pub const memfd_allocator = @import("./linux_memfd_allocator.zig").LinuxMemFdAllocator; + pub const memfd_allocator = @import("./allocators/linux_memfd_allocator.zig").LinuxMemFdAllocator; }; /// hash a string @@ -887,7 +887,7 @@ pub fn openDirAbsoluteNotForDeletingOrRenaming(path_: []const u8) !std.fs.Dir { return fd.asDir(); } -pub const MimallocArena = @import("./mimalloc_arena.zig").Arena; +pub const MimallocArena = @import("./allocators/mimalloc_arena.zig").Arena; pub fn getRuntimeFeatureFlag(comptime flag: [:0]const u8) bool { return struct { const state = enum(u8) { idk, disabled, enabled }; @@ -1607,7 +1607,7 @@ pub const fast_debug_build_mode = fast_debug_build_cmd != .None and pub const MultiArrayList = @import("./multi_array_list.zig").MultiArrayList; pub const StringJoiner = @import("./StringJoiner.zig"); -pub const NullableAllocator = @import("./NullableAllocator.zig"); +pub const NullableAllocator = @import("./allocators/NullableAllocator.zig"); pub const renamer = @import("./renamer.zig"); // TODO: Rename to SourceMap as this is a struct. @@ -2044,7 +2044,7 @@ pub fn HiveRef(comptime T: type, comptime capacity: u16) type { }; } -pub const MaxHeapAllocator = @import("./max_heap_allocator.zig").MaxHeapAllocator; +pub const MaxHeapAllocator = @import("./allocators/max_heap_allocator.zig").MaxHeapAllocator; pub const tracy = @import("./tracy.zig"); pub const trace = tracy.trace; diff --git a/src/bun_js.zig b/src/bun_js.zig index 7ac6f5d26c19de..dae06365549e5d 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -29,7 +29,7 @@ const DotEnv = @import("env_loader.zig"); const which = @import("which.zig").which; const JSC = bun.JSC; const AsyncHTTP = bun.http.AsyncHTTP; -const Arena = @import("./mimalloc_arena.zig").Arena; +const Arena = @import("./allocators/mimalloc_arena.zig").Arena; const OpaqueWrap = JSC.OpaqueWrap; const VirtualMachine = JSC.VirtualMachine; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 99d7e0a03a7c27..6cd32b15dda9de 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -71,7 +71,7 @@ const Ref = @import("../ast/base.zig").Ref; const Define = @import("../defines.zig").Define; const DebugOptions = @import("../cli.zig").Command.DebugOptions; const ThreadPoolLib = @import("../thread_pool.zig"); -const ThreadlocalArena = @import("../mimalloc_arena.zig").Arena; +const ThreadlocalArena = @import("../allocators/mimalloc_arena.zig").Arena; const BabyList = @import("../baby_list.zig").BabyList; const Fs = @import("../fs.zig"); const schema = @import("../api/schema.zig"); diff --git a/src/css/css_internals.zig b/src/css/css_internals.zig index 09a86a80a08fc6..4c0d86dc45cd1c 100644 --- a/src/css/css_internals.zig +++ b/src/css/css_internals.zig @@ -1,7 +1,7 @@ const bun = @import("root").bun; const std = @import("std"); const builtin = @import("builtin"); -const Arena = @import("../mimalloc_arena.zig").Arena; +const Arena = @import("../allocators/mimalloc_arena.zig").Arena; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const JSC = bun.JSC; diff --git a/src/http.zig b/src/http.zig index 47339a45435814..49e077a0376945 100644 --- a/src/http.zig +++ b/src/http.zig @@ -27,7 +27,7 @@ const ThreadPool = bun.ThreadPool; const ObjectPool = @import("./pool.zig").ObjectPool; const posix = std.posix; const SOCK = posix.SOCK; -const Arena = @import("./mimalloc_arena.zig").Arena; +const Arena = @import("./allocators/mimalloc_arena.zig").Arena; const ZlibPool = @import("./http/zlib.zig"); const BoringSSL = bun.BoringSSL; const Progress = bun.Progress; diff --git a/src/install/install.zig b/src/install/install.zig index 546dbf2ef478b7..6464ccac495778 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -57,7 +57,7 @@ const clap = bun.clap; const ExtractTarball = @import("./extract_tarball.zig"); pub const Npm = @import("./npm.zig"); const Bitset = bun.bit_set.DynamicBitSetUnmanaged; -const z_allocator = @import("../memory_allocator.zig").z_allocator; +const z_allocator = @import("../allocators/memory_allocator.zig").z_allocator; const Syscall = bun.sys; const RunCommand = @import("../cli/run_command.zig").RunCommand; const PackageManagerCommand = @import("../cli/package_manager_command.zig").PackageManagerCommand; diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index dad78519540bfe..63b21143522d1c 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -51,7 +51,7 @@ const clap = bun.clap; const ExtractTarball = @import("./extract_tarball.zig"); const Npm = @import("./npm.zig"); const Bitset = bun.bit_set.DynamicBitSetUnmanaged; -const z_allocator = @import("../memory_allocator.zig").z_allocator; +const z_allocator = @import("../allocators/memory_allocator.zig").z_allocator; const Lockfile = @This(); const IdentityContext = @import("../identity_context.zig").IdentityContext; diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index d8df1e8938865b..5f5a28b04d05af 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -161,8 +161,26 @@ declare function $toPropertyKey(x: any): PropertyKey; * `$toObject(this, "Class.prototype.method requires that |this| not be null or undefined");` */ declare function $toObject(object: any, errorMessage?: string): object; +/** + * ## References + * - [WebKit - `emit_intrinsic_newArrayWithSize`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2317) + */ declare function $newArrayWithSize(size: number): T[]; -declare function $newArrayWithSpecies(): TODO; +/** + * Optimized path for creating a new array storing objects with the same homogenous Structure + * as {@link array}. + * + * @param size the initial size of the new array + * @param array the array whose shape we want to copy + * + * @returns a new array + * + * ## References + * - [WebKit - `emit_intrinsic_newArrayWithSpecies`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2328) + * - [WebKit - #4909](https://github.com/WebKit/WebKit/pull/4909) + * - [WebKit Bugzilla - Related Issue/Ticket](https://bugs.webkit.org/show_bug.cgi?id=245797) + */ +declare function $newArrayWithSpecies(size: number, array: T[]): T[]; declare function $newPromise(): TODO; declare function $createPromise(): TODO; declare const $iterationKindKey: TODO; diff --git a/src/js/builtins/StreamInternals.ts b/src/js/builtins/StreamInternals.ts index a81ff3ca6f7d47..73b85878b84718 100644 --- a/src/js/builtins/StreamInternals.ts +++ b/src/js/builtins/StreamInternals.ts @@ -89,8 +89,8 @@ export function validateAndNormalizeQueuingStrategy(size, highWaterMark) { $linkTimeConstant; export function createFIFO() { - const Denqueue = require("internal/fifo"); - return new Denqueue(); + const Dequeue = require("internal/fifo"); + return new Dequeue(); } export function newQueue() { diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 893ff59006ff8f..fb9d0391e1680c 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -151,3 +151,4 @@ export const bindgen = $zig("bindgen_test.zig", "getBindgenTestFunctions") as { }; export const noOpForTesting = $cpp("NoOpForTesting.cpp", "createNoOpForTesting"); +export const Dequeue = require("internal/fifo"); diff --git a/src/js/internal/fifo.ts b/src/js/internal/fifo.ts index a7438aa3bdc698..c9ead667060e13 100644 --- a/src/js/internal/fifo.ts +++ b/src/js/internal/fifo.ts @@ -1,5 +1,10 @@ var slice = Array.prototype.slice; -class Denqueue { +class Dequeue { + _head: number; + _tail: number; + _capacityMask: number; + _list: (T | undefined)[]; + constructor() { this._head = 0; this._tail = 0; @@ -8,26 +13,21 @@ class Denqueue { this._list = $newArrayWithSize(4); } - _head; - _tail; - _capacityMask; - _list; - - size() { + size(): number { if (this._head === this._tail) return 0; if (this._head < this._tail) return this._tail - this._head; else return this._capacityMask + 1 - (this._head - this._tail); } - isEmpty() { + isEmpty(): boolean { return this.size() == 0; } - isNotEmpty() { + isNotEmpty(): boolean { return this.size() > 0; } - shift() { + shift(): T | undefined { var { _head: head, _tail, _list, _capacityMask } = this; if (head === _tail) return undefined; var item = _list[head]; @@ -37,24 +37,21 @@ class Denqueue { return item; } - peek() { + peek(): T | undefined { if (this._head === this._tail) return undefined; return this._list[this._head]; } - push(item) { + push(item: T): void { var tail = this._tail; $putByValDirect(this._list, tail, item); this._tail = (tail + 1) & this._capacityMask; if (this._tail === this._head) { this._growArray(); } - // if (this._capacity && this.size() > this._capacity) { - // this.shift(); - // } } - toArray(fullCopy) { + toArray(fullCopy: boolean): T[] { var list = this._list; var len = $toLength(list.length); @@ -66,19 +63,19 @@ class Denqueue { var j = 0; for (var i = _head; i < len; i++) $putByValDirect(array, j++, list[i]); for (var i = 0; i < _tail; i++) $putByValDirect(array, j++, list[i]); - return array; + return array as T[]; } else { return slice.$call(list, this._head, this._tail); } } - clear() { + clear(): void { this._head = 0; this._tail = 0; this._list.fill(undefined); } - _growArray() { + private _growArray(): void { if (this._head) { // copy existing data, head to end, then beginning to tail. this._list = this.toArray(true); @@ -92,10 +89,10 @@ class Denqueue { this._capacityMask = (this._capacityMask << 1) | 1; } - _shrinkArray() { + private _shrinkArray(): void { this._list.length >>>= 1; this._capacityMask >>>= 1; } } -export default Denqueue; +export default Dequeue; diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 46c29c17ba046d..61c85e22501f9c 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -1520,6 +1520,10 @@ class ClientRequest extends OutgoingMessage { return this.#agent; } + set agent(value) { + this.#agent = value; + } + #createStream() { if (!this.#stream) { var self = this; diff --git a/src/js_ast.zig b/src/js_ast.zig index 4263c9a60c8517..331e0bbc1d84f7 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -26,7 +26,7 @@ const ComptimeStringMap = bun.ComptimeStringMap; const JSPrinter = @import("./js_printer.zig"); const js_lexer = @import("./js_lexer.zig"); const TypeScript = @import("./js_parser.zig").TypeScript; -const ThreadlocalArena = @import("./mimalloc_arena.zig").Arena; +const ThreadlocalArena = @import("./allocators/mimalloc_arena.zig").Arena; const MimeType = bun.http.MimeType; const OOM = bun.OOM; const Loader = bun.options.Loader; diff --git a/src/main_wasm.zig b/src/main_wasm.zig index 3a1cfc5c833573..f94e2bc914001c 100644 --- a/src/main_wasm.zig +++ b/src/main_wasm.zig @@ -205,7 +205,7 @@ export fn init(heapsize: u32) void { buffer_writer = writer.ctx; } } -const Arena = @import("./mimalloc_arena.zig").Arena; +const Arena = @import("./allocators/mimalloc_arena.zig").Arena; var log: Logger.Log = undefined; diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 46f2dcd03a4aff..daa531a1c7618b 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -2,7 +2,7 @@ const tester = @import("../test/tester.zig"); const std = @import("std"); const strings = @import("../string_immutable.zig"); const FeatureFlags = @import("../feature_flags.zig"); -const default_allocator = @import("../memory_allocator.zig").c_allocator; +const default_allocator = @import("../allocators/memory_allocator.zig").c_allocator; const bun = @import("root").bun; const Fs = @import("../fs.zig"); diff --git a/test/internal/fifo.test.ts b/test/internal/fifo.test.ts new file mode 100644 index 00000000000000..9efd777dc71703 --- /dev/null +++ b/test/internal/fifo.test.ts @@ -0,0 +1,245 @@ +import { Dequeue } from "bun:internal-for-testing"; +import { describe, expect, test, it, beforeAll, beforeEach } from "bun:test"; + +/** + * Implements the same API as {@link Dequeue} but uses a simple list as the + * backing store. + * + * Used to check expected behavior. + */ +class DequeueList { + private _list: T[]; + + constructor() { + this._list = []; + } + + size(): number { + return this._list.length; + } + + isEmpty(): boolean { + return this.size() == 0; + } + + isNotEmpty(): boolean { + return this.size() > 0; + } + + shift(): T | undefined { + return this._list.shift(); + } + + peek(): T | undefined { + return this._list[0]; + } + + push(item: T): void { + this._list.push(item); + } + + toArray(fullCopy: boolean): T[] { + return fullCopy ? this._list.slice() : this._list; + } + + clear(): void { + this._list = []; + } +} + +describe("Given an empty queue", () => { + let queue: Dequeue; + + beforeEach(() => { + queue = new Dequeue(); + }); + + it("has a size of 0", () => { + expect(queue.size()).toBe(0); + }); + + it("is empty", () => { + expect(queue.isEmpty()).toBe(true); + expect(queue.isNotEmpty()).toBe(false); + }); + + it("shift() returns undefined", () => { + expect(queue.shift()).toBe(undefined); + expect(queue.size()).toBe(0); + }); + + it("has an initial capacity of 4", () => { + expect(queue._list.length).toBe(4); + expect(queue._capacityMask).toBe(3); + }); + + it("toArray() returns an empty array", () => { + expect(queue.toArray()).toEqual([]); + }); + + describe("When an element is pushed", () => { + beforeEach(() => { + queue.push(42); + }); + + it("has a size of 1", () => { + expect(queue.size()).toBe(1); + }); + + it("can be peeked without removing it", () => { + expect(queue.peek()).toBe(42); + expect(queue.size()).toBe(1); + }); + + it("is not empty", () => { + expect(queue.isEmpty()).toBe(false); + expect(queue.isNotEmpty()).toBe(true); + }); + + it("can be shifted out", () => { + const el = queue.shift(); + expect(el).toBe(42); + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBe(true); + }); + }); // +}); // + +describe("grow boundary conditions", () => { + describe.each([3, 4, 16])("when %d items are pushed", n => { + let queue: Dequeue; + + beforeEach(() => { + queue = new Dequeue(); + for (let i = 0; i < n; i++) { + queue.push(i); + } + }); + + it(`has a size of ${n}`, () => { + expect(queue.size()).toBe(n); + }); + + it("is not empty", () => { + expect(queue.isEmpty()).toBe(false); + expect(queue.isNotEmpty()).toBe(true); + }); + + it(`can shift() ${n} times`, () => { + for (let i = 0; i < n; i++) { + expect(queue.peek()).toBe(i); + expect(queue.shift()).toBe(i); + } + expect(queue.size()).toBe(0); + expect(queue.shift()).toBe(undefined); + }); + + it("toArray() returns [0..n-1]", () => { + // same as repeated push() but only allocates once + var expected = new Array(n); + for (let i = 0; i < n; i++) { + expected[i] = i; + } + expect(queue.toArray()).toEqual(expected); + }); + }); +}); // + +describe("adding and removing items", () => { + let queue: Dequeue; + let expected: DequeueList; + + describe("when 10k items are pushed", () => { + beforeEach(() => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 10_000; i++) { + queue.push(i); + expected.push(i); + } + }); + + it("has a size of 10000", () => { + expect(queue.size()).toBe(10_000); + expect(expected.size()).toBe(10_000); + }); + + describe("when 10 items are shifted", () => { + beforeEach(() => { + for (let i = 0; i < 10; i++) { + expect(queue.shift()).toBe(expected.shift()); + } + }); + + it("has a size of 9990", () => { + expect(queue.size()).toBe(9990); + expect(expected.size()).toBe(9990); + }); + }); + }); // + + describe("when 1k items are pushed, then removed", () => { + beforeEach(() => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 1_000; i++) { + queue.push(i); + expected.push(i); + } + expect(queue.size()).toBe(1_000); + + while (queue.isNotEmpty()) { + expect(queue.shift()).toBe(expected.shift()); + } + }); + + it("is now empty", () => { + expect(queue.size()).toBe(0); + expect(queue.isEmpty()).toBeTrue(); + expect(queue.isNotEmpty()).toBeFalse(); + }); + + it("when new items are added, the backing list is resized", () => { + for (let i = 0; i < 10_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + }); + }); // + + it("pushing and shifting a lot of items affects the size and backing list correctly", () => { + queue = new Dequeue(); + expected = new DequeueList(); + + for (let i = 0; i < 15_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + + // shift() shrinks the backing array when tail > 10,000 and the list is + // shrunk too far (tail <= list.length >>> 2) + for (let i = 0; i < 10_000; i++) { + expect(queue.shift()).toBe(expected.shift()); + expect(queue.size()).toBe(expected.size()); + } + + for (let i = 0; i < 5_000; i++) { + queue.push(i); + expected.push(i); + expect(queue.size()).toBe(expected.size()); + expect(queue.peek()).toBe(expected.peek()); + expect(queue.isEmpty()).toBeFalse(); + expect(queue.isNotEmpty()).toBeTrue(); + } + }); // +}); //