Compare commits

...

5 Commits

Author SHA1 Message Date
Dylan Conway
40acc7d524 update 2026-02-02 21:59:07 -08:00
Chris Lloyd
d23312d3f6 feat(test): add Symbol.dispose support to mock/spyOn (#26692)
## Summary

- Add `[Symbol.dispose]` to mock function prototype, aliased to
`mockRestore`
- Enables `using spy = spyOn(obj, "method")` to auto-restore when
leaving scope
- Works for both `spyOn()` and `mock()`

Addresses #6040 — gives users a clean way to scope spy lifetimes instead
of manually calling `mockRestore()` or relying on `afterEach`.

### Example

```ts
import { spyOn, expect, test } from "bun:test";

test("auto-restores spy", () => {
  const obj = { method: () => "original" };

  {
    using spy = spyOn(obj, "method").mockReturnValue("mocked");
    expect(obj.method()).toBe("mocked");
  }

  // automatically restored
  expect(obj.method()).toBe("original");
});
```

## Test plan

- `bun bd test test/js/bun/test/mock-disposable.test.ts` — 3 tests pass
- Verified tests fail with `USE_SYSTEM_BUN=1`
2026-02-02 17:39:36 -08:00
SUZUKI Sosuke
de8c754c6a perf(markdown): cache tag strings in React renderer (#26668)
## Summary

Cache frequently-used HTML tag strings (div, p, h1-h6, etc.) in
`GlobalObject` using `LazyProperty<JSGlobalObject, JSString>` instead of
creating new JSStrings on every React element creation in
`Bun.markdown.react()`.

## Changes

- Added `BunMarkdownTagStrings.h/.cpp` with 30 cached tag strings
- Modified `MarkdownObject.zig` to use cached strings via C API
- Integrated with `ZigGlobalObject` for proper GC visiting

## Benchmark Results

All benchmarks performed on Apple M4 Max with release builds.

### mitata Benchmark (Bun.markdown.react)

| Size | Main | Feature | Improvement |
|------|------|---------|-------------|
| small (121 chars) | 3.20 µs | 2.30 µs | **28% faster** |
| medium (1039 chars) | 15.09 µs | 14.02 µs | **7% faster** |
| large (20780 chars) | 288.48 µs | 267.14 µs | **7.4% faster** |

### Heap Profile

| Metric | Main | Feature | Improvement |
|--------|------|---------|-------------|
| Heap size | 500.7 KB | 469.7 KB | **6% reduction** |
| Object count | 12,000 | 10,315 | **14% reduction** |
| String count | 4,248 | 2,563 | **40% reduction** |
| String size | 97.1 KB | 65.8 KB | **32% reduction** |

### HTTP Request Benchmark (ab -n 10000 -c 20)

| Metric | Main | Feature | Improvement |
|--------|------|---------|-------------|
| Requests/sec | 7,710 | 8,174 | **6% faster** |
| Time/request | 2.59 ms | 2.45 ms | **5% faster** |
| p99 latency | 6 ms | 3 ms | **50% improvement** |

## Technical Details

The optimization uses JSC's `LazyProperty` pattern (similar to
`BunCommonStrings` and `BunHttp2CommonStrings`) to lazily initialize and
cache tag strings on first use. This avoids repeated
`bun.String.createUTF8ForJS` calls which allocate new JSStrings for the
same tag names on every markdown element.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-02 17:08:39 -08:00
Jarred Sumner
27e1363a66 Make GC release access skip interval configurable (#26699)
## Summary
- Make `defaultRemainingRunsUntilSkipReleaseAccess` configurable at
runtime instead of a compile-time constant
- Add `BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS` environment variable to
control how many idle event loop iterations pass before skipping JSC
heap `releaseAccess` calls in `onBeforeWait`
- Default remains 10, matching the previous hardcoded value

## Test plan
- [ ] Verify default behavior is unchanged (no env var set, value is 10)
- [ ] Verify `BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS=0` causes release
access to be skipped every iteration
- [ ] Verify `BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS=100` delays skipping
for 100 idle iterations
- [ ] Verify negative values are ignored (default is preserved)
- [ ] Verify non-numeric values are ignored (default is preserved)

## Changelog
<!-- CHANGELOG:START -->
<!-- CHANGELOG:END -->

🤖 Generated with [Claude Code](https://claude.com/claude-code) (0%
3-shotted by claude)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 17:07:20 -08:00
SUZUKI Sosuke
eba4da23e6 perf: skip Event creation in AbortSignal.abort() when no listeners (#26686)
## Summary

This is a tiny optimization that skips creating and dispatching an Event
object when `AbortSignal.abort()` is called with no registered
listeners.

## Changes

When there are no listeners (no `addEventListener` or `onabort`), we now
check `hasEventListeners()` before creating the Event, avoiding:
- Event object allocation (~112 bytes)
- EventPath creation
- dispatchEvent overhead (hash map lookups, method calls)

## Performance

Improvement for the no-listener case:
- **~6% faster** in micro-benchmarks
- ~16ms saved per 1M `abort()` calls (271ms → 255ms)

| Case | Before | After | Improvement |
|------|--------|-------|-------------|
| no listener | 271 ms | 255 ms | ~6% |
| with listener | 368 ms | 370 ms | (same) |

## Why this is safe

The optimization has no observable side effects because:
- `dispatchEvent` is called from C++, not observable via JS
monkey-patching
- Without listeners, no code can obtain a reference to the Event object
- All internal state (`aborted`, `reason`) is set correctly regardless

## Test Plan

- Existing AbortController/AbortSignal tests pass
- Added mitata benchmark: `bench/snippets/abort-signal.mjs`
2026-02-02 14:01:25 -08:00
16 changed files with 406 additions and 62 deletions

View File

@@ -0,0 +1,110 @@
// Benchmark for AbortController/AbortSignal abort() performance
// Tests the optimization of skipping Event creation when no listeners are registered
import { bench, group, run } from "../runner.mjs";
// Warmup: ensure JIT compilation
for (let i = 0; i < 1000; i++) {
const controller = new AbortController();
controller.abort();
}
group("AbortController.abort()", () => {
bench("no listener", () => {
const controller = new AbortController();
controller.abort();
});
bench("with addEventListener", () => {
const controller = new AbortController();
controller.signal.addEventListener("abort", () => {});
controller.abort();
});
bench("with onabort property", () => {
const controller = new AbortController();
controller.signal.onabort = () => {};
controller.abort();
});
bench("with 3 listeners", () => {
const controller = new AbortController();
controller.signal.addEventListener("abort", () => {});
controller.signal.addEventListener("abort", () => {});
controller.signal.addEventListener("abort", () => {});
controller.abort();
});
});
group("AbortSignal static methods", () => {
bench("AbortSignal.abort() - pre-aborted", () => {
const signal = AbortSignal.abort();
// Signal is already aborted, no event dispatch needed
});
bench("AbortSignal.any([]) - empty array", () => {
const signal = AbortSignal.any([]);
});
bench("AbortSignal.any([signal, signal]) - 2 signals", () => {
const a = new AbortController();
const b = new AbortController();
const signal = AbortSignal.any([a.signal, b.signal]);
});
bench("AbortSignal.any() then abort - no listener", () => {
const a = new AbortController();
const b = new AbortController();
const signal = AbortSignal.any([a.signal, b.signal]);
a.abort();
});
bench("AbortSignal.any() then abort - with listener", () => {
const a = new AbortController();
const b = new AbortController();
const signal = AbortSignal.any([a.signal, b.signal]);
signal.addEventListener("abort", () => {});
a.abort();
});
});
group("AbortController creation only", () => {
bench("new AbortController()", () => {
const controller = new AbortController();
});
bench("new AbortController() + access signal", () => {
const controller = new AbortController();
const signal = controller.signal;
});
});
group("AbortSignal.timeout()", () => {
// Note: These don't actually wait for timeout, just measure creation overhead
bench("AbortSignal.timeout(1000) creation", () => {
const signal = AbortSignal.timeout(1000);
});
bench("AbortSignal.timeout(0) creation", () => {
const signal = AbortSignal.timeout(0);
});
});
group("abort with reason", () => {
bench("abort() with no reason", () => {
const controller = new AbortController();
controller.abort();
});
bench("abort() with string reason", () => {
const controller = new AbortController();
controller.abort("cancelled");
});
bench("abort() with Error reason", () => {
const controller = new AbortController();
controller.abort(new Error("cancelled"));
});
});
await run();

View File

@@ -105,6 +105,10 @@ summary(() => {
bench(`small (${small.length} chars) - Bun.markdown.render`, () => {
return Bun.markdown.render(small, renderCallbacks);
});
bench(`small (${small.length} chars) - Bun.markdown.react`, () => {
return Bun.markdown.react(small);
});
}
bench(`small (${small.length} chars) - marked`, () => {
@@ -125,6 +129,10 @@ summary(() => {
bench(`medium (${medium.length} chars) - Bun.markdown.render`, () => {
return Bun.markdown.render(medium, renderCallbacks);
});
bench(`medium (${medium.length} chars) - Bun.markdown.react`, () => {
return Bun.markdown.react(medium);
});
}
bench(`medium (${medium.length} chars) - marked`, () => {
@@ -145,6 +153,10 @@ summary(() => {
bench(`large (${large.length} chars) - Bun.markdown.render`, () => {
return Bun.markdown.render(large, renderCallbacks);
});
bench(`large (${large.length} chars) - Bun.markdown.react`, () => {
return Bun.markdown.react(large);
});
}
bench(`large (${large.length} chars) - marked`, () => {

View File

@@ -2179,6 +2179,7 @@ declare module "bun:test" {
mockResolvedValueOnce(value: ResolveType<T>): this;
mockRejectedValue(value: RejectType<T>): this;
mockRejectedValueOnce(value: RejectType<T>): this;
[Symbol.dispose](): void;
}
// export type MockMetadata<T, MetadataType = MockMetadataType> = {

View File

@@ -7,6 +7,7 @@ const VirtualMachine = @This();
export var has_bun_garbage_collector_flag_enabled = false;
pub export var isBunTest: bool = false;
pub export var Bun__defaultRemainingRunsUntilSkipReleaseAccess: c_int = 10;
// TODO: evaluate if this has any measurable performance impact.
pub var synthetic_allocation_limit: usize = std.math.maxInt(u32);

View File

@@ -464,8 +464,8 @@ const ParseRenderer = struct {
const entry = self.#stack.pop().?;
const g = self.#globalObject;
// Determine HTML tag name
const type_str: []const u8 = blockTypeName(block_type, entry.data);
// Determine HTML tag index for cached string
const tag_index = getBlockTypeTag(block_type, entry.data);
// For headings, compute slug before counting props
const slug: ?[]const u8 = if (block_type == .h) self.#heading_tracker.leaveHeading(bun.default_allocator) else null;
@@ -496,7 +496,7 @@ const ParseRenderer = struct {
// Build React element — use component override as type if set
const component = self.getBlockComponent(block_type, entry.data);
const type_val: JSValue = if (component != .zero) component else try bun.String.createUTF8ForJS(g, type_str);
const type_val: JSValue = if (component != .zero) component else getCachedTagString(g, tag_index);
const props = JSValue.createEmptyObject(g, props_count);
self.#marked_args.append(props);
@@ -572,7 +572,7 @@ const ParseRenderer = struct {
const entry = self.#stack.pop().?;
const g = self.#globalObject;
const type_str: []const u8 = spanTypeName(span_type);
const tag_index = getSpanTypeTag(span_type);
// Count props fields: always children (or alt for img) + metadata
var props_count: usize = 1; // children (or alt for img)
@@ -592,7 +592,7 @@ const ParseRenderer = struct {
// Build React element: { $$typeof, type, key, ref, props }
const component = self.getSpanComponent(span_type);
const type_val: JSValue = if (component != .zero) component else try bun.String.createUTF8ForJS(g, type_str);
const type_val: JSValue = if (component != .zero) component else getCachedTagString(g, tag_index);
const props = JSValue.createEmptyObject(g, props_count);
self.#marked_args.append(props);
@@ -675,7 +675,7 @@ const ParseRenderer = struct {
switch (text_type) {
.br => {
const br_component = self.#components.br;
const br_type: JSValue = if (br_component != .zero) br_component else try bun.String.createUTF8ForJS(g, "br");
const br_type: JSValue = if (br_component != .zero) br_component else getCachedTagString(g, .br);
const empty_props = JSValue.createEmptyObject(g, 0);
self.#marked_args.append(empty_props);
const obj = self.createElement(br_type, empty_props);
@@ -705,53 +705,6 @@ const ParseRenderer = struct {
},
}
}
// ========================================
// Type name mappings
// ========================================
fn blockTypeName(block_type: md.BlockType, data: u32) []const u8 {
return switch (block_type) {
.h => switch (data) {
1 => "h1",
2 => "h2",
3 => "h3",
4 => "h4",
5 => "h5",
else => "h6",
},
.p => "p",
.quote => "blockquote",
.ul => "ul",
.ol => "ol",
.li => "li",
.code => "pre",
.hr => "hr",
.html => "html",
.table => "table",
.thead => "thead",
.tbody => "tbody",
.tr => "tr",
.th => "th",
.td => "td",
.doc => "div",
};
}
fn spanTypeName(span_type: md.SpanType) []const u8 {
return switch (span_type) {
.em => "em",
.strong => "strong",
.a => "a",
.img => "img",
.code => "code",
.del => "del",
.latexmath => "math",
.latexmath_display => "math",
.wikilink => "a",
.u => "u",
};
}
};
/// Renderer that calls JavaScript callbacks for each markdown element.
@@ -1125,6 +1078,89 @@ fn extractLanguage(src_text: []const u8, info_beg: u32) []const u8 {
return "";
}
// Cached tag string indices - must match BunMarkdownTagStrings.h
const TagIndex = enum(u8) {
h1 = 0,
h2 = 1,
h3 = 2,
h4 = 3,
h5 = 4,
h6 = 5,
p = 6,
blockquote = 7,
ul = 8,
ol = 9,
li = 10,
pre = 11,
hr = 12,
html = 13,
table = 14,
thead = 15,
tbody = 16,
tr = 17,
th = 18,
td = 19,
div = 20,
em = 21,
strong = 22,
a = 23,
img = 24,
code = 25,
del = 26,
math = 27,
u = 28,
br = 29,
};
extern fn BunMarkdownTagStrings__getTagString(*jsc.JSGlobalObject, u8) JSValue;
fn getCachedTagString(globalObject: *jsc.JSGlobalObject, tag: TagIndex) JSValue {
return BunMarkdownTagStrings__getTagString(globalObject, @intFromEnum(tag));
}
fn getBlockTypeTag(block_type: md.BlockType, data: u32) TagIndex {
return switch (block_type) {
.h => switch (data) {
1 => .h1,
2 => .h2,
3 => .h3,
4 => .h4,
5 => .h5,
else => .h6,
},
.p => .p,
.quote => .blockquote,
.ul => .ul,
.ol => .ol,
.li => .li,
.code => .pre,
.hr => .hr,
.html => .html,
.table => .table,
.thead => .thead,
.tbody => .tbody,
.tr => .tr,
.th => .th,
.td => .td,
.doc => .div,
};
}
fn getSpanTypeTag(span_type: md.SpanType) TagIndex {
return switch (span_type) {
.em => .em,
.strong => .strong,
.a => .a,
.img => .img,
.code => .code,
.del => .del,
.latexmath => .math,
.latexmath_display => .math,
.wikilink => .a,
.u => .u,
};
}
const std = @import("std");
const bun = @import("bun");

View File

@@ -4,6 +4,8 @@
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/Heap.h>
extern "C" int Bun__defaultRemainingRunsUntilSkipReleaseAccess;
extern "C" void Bun__JSC_onBeforeWait(JSC::VM* _Nonnull vm)
{
ASSERT(vm);
@@ -46,7 +48,7 @@ extern "C" void Bun__JSC_onBeforeWait(JSC::VM* _Nonnull vm)
// finalizers that might've been waiting to be run is a good idea.
// But if you haven't, like if the process is just waiting on I/O
// then don't bother.
static constexpr int defaultRemainingRunsUntilSkipReleaseAccess = 10;
const int defaultRemainingRunsUntilSkipReleaseAccess = Bun__defaultRemainingRunsUntilSkipReleaseAccess;
static thread_local int remainingRunsUntilSkipReleaseAccess = 0;

View File

@@ -0,0 +1,59 @@
#include "root.h"
#include "BunMarkdownTagStrings.h"
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/LazyProperty.h>
#include <JavaScriptCore/LazyPropertyInlines.h>
#include "ZigGlobalObject.h"
#include <JavaScriptCore/SlotVisitorInlines.h>
#include <JavaScriptCore/VMTrapsInlines.h>
namespace Bun {
using namespace JSC;
#define MARKDOWN_TAG_STRINGS_LAZY_PROPERTY_DEFINITION(name, str, idx) \
this->m_strings[idx].initLater( \
[](const JSC::LazyProperty<JSGlobalObject, JSString>::Initializer& init) { \
init.set(jsOwnedString(init.vm, str)); \
});
#define MARKDOWN_TAG_STRINGS_LAZY_PROPERTY_VISITOR(name, str, idx) \
this->m_strings[idx].visit(visitor);
void MarkdownTagStrings::initialize()
{
MARKDOWN_TAG_STRINGS_EACH_NAME(MARKDOWN_TAG_STRINGS_LAZY_PROPERTY_DEFINITION)
}
template<typename Visitor>
void MarkdownTagStrings::visit(Visitor& visitor)
{
MARKDOWN_TAG_STRINGS_EACH_NAME(MARKDOWN_TAG_STRINGS_LAZY_PROPERTY_VISITOR)
}
template void MarkdownTagStrings::visit(JSC::AbstractSlotVisitor&);
template void MarkdownTagStrings::visit(JSC::SlotVisitor&);
} // namespace Bun
// C API for Zig bindings
extern "C" JSC::EncodedJSValue BunMarkdownTagStrings__getTagString(Zig::GlobalObject* globalObject, uint8_t tagIndex)
{
if (tagIndex >= MARKDOWN_TAG_STRINGS_COUNT)
return JSC::JSValue::encode(JSC::jsUndefined());
auto& tagStrings = globalObject->markdownTagStrings();
// Use a switch to call the appropriate accessor
switch (tagIndex) {
#define MARKDOWN_TAG_STRINGS_CASE(name, str, idx) \
case idx: \
return JSC::JSValue::encode(tagStrings.name##String(globalObject));
MARKDOWN_TAG_STRINGS_EACH_NAME(MARKDOWN_TAG_STRINGS_CASE)
#undef MARKDOWN_TAG_STRINGS_CASE
default:
return JSC::JSValue::encode(JSC::jsUndefined());
}
}

View File

@@ -0,0 +1,70 @@
#pragma once
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/LazyProperty.h>
// Markdown HTML tag names cached as JSStrings
// These are commonly reused when rendering markdown to React elements
// clang-format off
#define MARKDOWN_TAG_STRINGS_EACH_NAME(macro) \
macro(h1, "h1"_s, 0) \
macro(h2, "h2"_s, 1) \
macro(h3, "h3"_s, 2) \
macro(h4, "h4"_s, 3) \
macro(h5, "h5"_s, 4) \
macro(h6, "h6"_s, 5) \
macro(p, "p"_s, 6) \
macro(blockquote, "blockquote"_s, 7) \
macro(ul, "ul"_s, 8) \
macro(ol, "ol"_s, 9) \
macro(li, "li"_s, 10) \
macro(pre, "pre"_s, 11) \
macro(hr, "hr"_s, 12) \
macro(html, "html"_s, 13) \
macro(table, "table"_s, 14) \
macro(thead, "thead"_s, 15) \
macro(tbody, "tbody"_s, 16) \
macro(tr, "tr"_s, 17) \
macro(th, "th"_s, 18) \
macro(td, "td"_s, 19) \
macro(div, "div"_s, 20) \
macro(em, "em"_s, 21) \
macro(strong, "strong"_s, 22) \
macro(a, "a"_s, 23) \
macro(img, "img"_s, 24) \
macro(code, "code"_s, 25) \
macro(del, "del"_s, 26) \
macro(math, "math"_s, 27) \
macro(u, "u"_s, 28) \
macro(br, "br"_s, 29)
// clang-format on
#define MARKDOWN_TAG_STRINGS_COUNT 30
namespace Bun {
using namespace JSC;
class MarkdownTagStrings {
public:
#define MARKDOWN_TAG_STRINGS_ACCESSOR_DEFINITION(name, str, idx) \
JSC::JSString* name##String(JSC::JSGlobalObject* globalObject) \
{ \
return m_strings[idx].getInitializedOnMainThread(globalObject); \
}
MARKDOWN_TAG_STRINGS_EACH_NAME(MARKDOWN_TAG_STRINGS_ACCESSOR_DEFINITION)
#undef MARKDOWN_TAG_STRINGS_ACCESSOR_DEFINITION
void initialize();
template<typename Visitor>
void visit(Visitor& visitor);
private:
JSC::LazyProperty<JSC::JSGlobalObject, JSC::JSString> m_strings[MARKDOWN_TAG_STRINGS_COUNT];
};
} // namespace Bun

View File

@@ -988,6 +988,10 @@ void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* g
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
this->putDirect(vm, Identifier::fromString(vm, "_isMockFunction"_s), jsBoolean(true), 0);
// Support `using spy = spyOn(...)` — auto-restores when leaving scope.
JSValue restoreFn = this->getDirect(vm, Identifier::fromString(vm, "mockRestore"_s));
this->putDirect(vm, vm.propertyNames->disposeSymbol, restoreFn, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum));
}
JSC_DEFINE_HOST_FUNCTION(jsMockFunctionGetMockImplementation, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))

View File

@@ -302,7 +302,6 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c
JSC::Options::useJITCage() = false;
JSC::Options::useShadowRealm() = true;
JSC::Options::useV8DateParser() = true;
JSC::Options::useMathSumPreciseMethod() = true;
JSC::Options::evalMode() = evalMode;
JSC::Options::heapGrowthSteepnessFactor() = 1.0;
JSC::Options::heapGrowthMaxIncrease() = 2.0;
@@ -1699,6 +1698,7 @@ void GlobalObject::finishCreation(VM& vm)
m_commonStrings.initialize();
m_http2CommonStrings.initialize();
m_bakeAdditions.initialize();
m_markdownTagStrings.initialize();
Bun::addNodeModuleConstructorProperties(vm, this);
m_JSNodeHTTPServerSocketStructure.initLater(

View File

@@ -58,6 +58,7 @@ struct node_module;
#include "headers-handwritten.h"
#include "BunCommonStrings.h"
#include "BunHttp2CommonStrings.h"
#include "BunMarkdownTagStrings.h"
#include "BunGlobalScope.h"
#include <js_native_api.h>
#include <node_api.h>
@@ -526,6 +527,7 @@ public:
V(private, std::unique_ptr<WebCore::DOMConstructors>, m_constructors) \
V(private, Bun::CommonStrings, m_commonStrings) \
V(private, Bun::Http2CommonStrings, m_http2CommonStrings) \
V(private, Bun::MarkdownTagStrings, m_markdownTagStrings) \
\
/* JSC's hashtable code-generator tries to access these properties, so we make them public. */ \
/* However, we'd like it better if they could be protected. */ \
@@ -716,6 +718,7 @@ public:
Bun::CommonStrings& commonStrings() { return m_commonStrings; }
Bun::Http2CommonStrings& http2CommonStrings() { return m_http2CommonStrings; }
Bun::MarkdownTagStrings& markdownTagStrings() { return m_markdownTagStrings; }
#include "ZigGeneratedClasses+lazyStructureHeader.h"
void finishCreation(JSC::VM&);

View File

@@ -171,8 +171,8 @@ void AbortSignal::runAbortSteps()
algorithm.second(reason);
// 3. Fire an event named abort at signal.
dispatchEvent(Event::create(eventNames().abortEvent, Event::CanBubble::No, Event::IsCancelable::No));
if (hasEventListeners(eventNames().abortEvent))
dispatchEvent(Event::create(eventNames().abortEvent, Event::CanBubble::No, Event::IsCancelable::No));
setIsFiringEventListeners(false);
}

View File

@@ -49,14 +49,14 @@ WebCoreTypedArrayController::WebCoreTypedArrayController(bool allowAtomicsWait)
WebCoreTypedArrayController::~WebCoreTypedArrayController() = default;
JSC::JSArrayBuffer* WebCoreTypedArrayController::toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSGlobalObject* globalObject, JSC::ArrayBuffer* buffer)
JSC::JSArrayBuffer* WebCoreTypedArrayController::toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSGlobalObject* globalObject, JSC::ArrayBuffer& buffer)
{
return JSC::jsCast<JSC::JSArrayBuffer*>(WebCore::toJS(lexicalGlobalObject, getDefaultGlobal(globalObject), buffer));
}
void WebCoreTypedArrayController::registerWrapper(JSC::JSGlobalObject* globalObject, JSC::ArrayBuffer* native, JSC::JSArrayBuffer* wrapper)
void WebCoreTypedArrayController::registerWrapper(JSC::JSGlobalObject* globalObject, JSC::ArrayBuffer& native, JSC::JSArrayBuffer& wrapper)
{
cacheWrapper(static_cast<JSVMClientData*>(JSC::getVM(globalObject).clientData)->normalWorld(), native, wrapper);
cacheWrapper(static_cast<JSVMClientData*>(JSC::getVM(globalObject).clientData)->normalWorld(), &native, &wrapper);
}
bool WebCoreTypedArrayController::isAtomicsWaitAllowedOnCurrentThread()

View File

@@ -35,8 +35,8 @@ public:
WebCoreTypedArrayController(bool allowAtomicsWait);
virtual ~WebCoreTypedArrayController();
JSC::JSArrayBuffer* toJS(JSC::JSGlobalObject*, JSC::JSGlobalObject*, JSC::ArrayBuffer*) override;
void registerWrapper(JSC::JSGlobalObject*, ArrayBuffer*, JSC::JSArrayBuffer*) override;
JSC::JSArrayBuffer* toJS(JSC::JSGlobalObject*, JSC::JSGlobalObject*, JSC::ArrayBuffer&) override;
void registerWrapper(JSC::JSGlobalObject*, ArrayBuffer&, JSC::JSArrayBuffer&) override;
bool isAtomicsWaitAllowedOnCurrentThread() override;
JSC::WeakHandleOwner* wrapperOwner() { return &m_owner; }

View File

@@ -52,6 +52,14 @@ pub fn init(this: *GarbageCollectionController, vm: *VirtualMachine) void {
}
this.gc_timer_interval = gc_timer_interval;
if (vm.transpiler.env.get("BUN_GC_RUNS_UNTIL_SKIP_RELEASE_ACCESS")) |val| {
if (std.fmt.parseInt(c_int, val, 10)) |parsed| {
if (parsed >= 0) {
VirtualMachine.Bun__defaultRemainingRunsUntilSkipReleaseAccess = parsed;
}
} else |_| {}
}
this.disabled = vm.transpiler.env.has("BUN_GC_TIMER_DISABLE");
if (!this.disabled)

View File

@@ -0,0 +1,38 @@
import { expect, mock, spyOn, test } from "bun:test";
test("spyOn returns a disposable that calls mockRestore", () => {
const obj = { method: () => "original" };
{
using spy = spyOn(obj, "method").mockReturnValue("mocked");
expect(obj.method()).toBe("mocked");
expect(spy).toHaveBeenCalledTimes(1);
}
expect(obj.method()).toBe("original");
});
test("mock() returns a disposable that calls mockRestore", () => {
const fn = mock(() => "original");
fn();
expect(fn).toHaveBeenCalledTimes(1);
expect(fn[Symbol.dispose]).toBeFunction();
fn[Symbol.dispose]();
expect(fn).toHaveBeenCalledTimes(0);
});
test("using with spyOn auto-restores prototype methods", () => {
class Greeter {
greet() {
return "hello";
}
}
{
using spy = spyOn(Greeter.prototype, "greet").mockReturnValue("hola");
expect(new Greeter().greet()).toBe("hola");
}
expect(new Greeter().greet()).toBe("hello");
});