npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tenure-as

v0.1.1

Published

Hybrid generational and tenure-arena garbage collector for AssemblyScript.

Downloads

267

Readme

tenure-as

Tenure is a standalone AssemblyScript hybrid garbage collector. Escaping and unknown objects use a non-moving generational collector; the included transform routes syntactically proven function-local allocations through bounded bump arenas. It requires no Wago runtime or host plugin.

Usage

asc assembly/index.ts \
  --runtime ./node_modules/tenure-as/runtime \
  --transform ./node_modules/tenure-as/transform

The current implementation is ABI 1. Its frozen compatibility entry points are ./runtime/index-legacy and ./transform/legacy.mjs; they compile the same legacy implementation while the next typed implementation is developed separately. The exact object, metadata, error, and future-barrier contract is in TENURE_ABI.md.

The typed compiler integration uses the companion assemblyscript-tenure fork. It adds an afterLayout transform phase and an opt-in compiler ABI for exact managed-field barriers. Point asc at that fork when using typed analysis:

TENURE_TYPED_DUMP=tenure-cfg.json /path/to/assemblyscript-tenure/bin/asc.js assembly/index.ts \
  --runtime ./node_modules/tenure-as/runtime \
  --transform ./node_modules/tenure-as/typed-analysis

The typed-analysis Wasm artifact also carries a versioned tenure.analysis custom section. Its TNA1 prefix, format version, and UTF-8 payload provide stable CFG block/value IDs, finalized layout facts, and conservatively identified managed-store barrier sites (including exact field offsets) to verifier or lowering tooling. With the fork, managed field setters lower directly to the existing __tenure_write_barrier(parent, offset, child) runtime ABI; no post-initialization AST calls or application-source annotations are needed.

./typed-region is the first compiler-owned placement experiment. It selects only resolved, call-free functions whose ownership graph proves every allocation local, then asks the fork to lower balanced region enter/exit calls. Scalar returns are materialized before exit; managed-reference returns and all other functions remain on the generational fallback heap. Set TENURE_REGION_DUMP=regions.json to emit a deterministic eligibility report with selected functions and reason-coded fallbacks. Defining AssemblyScript's global __finalize hook disables typed-region placement for that module, preserving normal finalization semantics.

Before loading a marked artifact in host tooling, run npm run verify-artifact -- module.wasm. It rejects malformed metadata and unsupported ABI/feature combinations rather than attempting compatibility.

./ownership-analysis is the next report-only layer. With TENURE_OWNERSHIP_DUMP=ownership.json, it runs a CFG fixed point and reports the conservative Fresh/Borrowed/Shared/Escaped/Unknown placement lattice plus a source-range reason for every fallback. It still emits no allocation rewrite.

The test suite also compiles allocation-heavy differential fixtures under both AssemblyScript incremental GC and Tenure, then compares observable results over fixed churn/control-flow schedules and isolated nested-graph, deep-root, and pinned-write corpora. It is a regression signal for the fallback heap; it is not a substitute for the planned randomized verifier.

This is source-level drop-in: application sources do not change. AssemblyScript still requires --runtime because transforms run after runtime selection. Young objects are collected frequently and surviving objects promote in place; a periodic full major collection reclaims old objects. __link records old/static parents that may point into young space.

Hosts that implement phase-boundary arena resets can select the stop-the-world runtime separately:

asc assembly/index.ts \
  --runtime ./node_modules/tenure-as/runtime-stw \
  --transform ./node_modules/tenure-as/phase-transform \
  --use ASC_ARENA_EXACT_GLOBALS=1

This runtime exports GUEST_FEATURE_TENURE_PHASE_ARENA; a supporting host calls __arena_reset only after copying each phase result out of Wasm memory and skips its normal scheduled __collect cadence. If a phase publishes an arena object directly to a managed global, links it into a normal-heap object, or pins it, reset preserves the current arena generation and starts a fresh reusable scratch generation. Referenced generations spend one phase in probation: still-referenced state becomes permanent, while overwritten phase-local globals rotate through two reusable generations. This makes late lazy initialization safe without moving published objects. __arena_escape_fallback_count exposes whether that safety path fired. The phase transform records exact managed global assignments so reset touches only assigned slots. ASC_ARENA_EXACT_GLOBALS=1 declares that the transform is present even when a module has no mutable or lazy globals. Artifacts built without it remain correct through the compiler's global visitor compatibility path, but reset is substantially more expensive for modules with large global tables.

The nursery starts at ASC_GC_GRANULARITY and expands to a bounded 8 MiB window after a mostly-live minor cycle. A garbage-producing cycle returns it to the configured baseline.

Compile with --enable simd to batch four shadow-stack slots per root-scan load and use a vector non-null mask to skip empty slots. Builds without SIMD use a two-slot u64 SWAR path, followed by a scalar tail. Object-member scanning remains exact and layout-specific in both modes; the current __link ABI does not expose a stored-field address, so it cannot maintain a conventional field-card table without an AssemblyScript compiler ABI extension.

Arena transform

The included conservative transform bump-allocates function-local objects. Completed standard chunks are retained in a bounded reuse batch, then drained by __collect or __arena_collect. Normal builds use 256 KiB chunks, up to eight retained chunks, and 256-call allocation batches; --lowMemoryLimit uses 64 KiB chunks, two retained chunks, and 64-call batches. ASC_ARENA_RETAIN_CHUNKS and ASC_ARENA_BATCH_CALLS override these defaults. It requires no source annotations:

asc assembly/index.ts \
  --runtime ./runtime/index-tenure \
  --transform ./transform/arena.mjs

The typed-region runtime uses a bounded, lazy direct linear-memory page pool by default (four pages normally, one with --lowMemoryLimit). To tune the hard cap or disable it for a host that cannot retain grown Wasm memory:

asc assembly/index.ts \
  --runtime ./runtime/index-tenure \
  --transform ./transform/arena.mjs \
  --use ASC_ARENA_LINEAR_PAGES=8 # or =0 to disable direct pages

Eligible short-lived objects then occupy raw 64 KiB pages acquired with memory.grow, bypassing TLSF and GC object-list insertion after a page refill. The page count is a hard cap: released pages are reused only by future arena scopes because Wasm memory cannot shrink. Once the cap is reached, Tenure falls back to the normal bounded TLSF-backed region chunks.

For an artifact-contained explanation of every region decision, add --use ASC_TENURE_DIAGNOSTICS=1. This emits an optional tenure.diagnostics custom section with selected functions, source ranges, and reason-coded conservative fallbacks; it does not change allocation semantics.

For debug verification, add --use ASC_TENURE_VERIFY=1. Direct pages are poisoned at region exit after return expressions have been evaluated, helping expose stale-region reads during test and fuzz runs.

It proves non-escape syntactically and falls back to normal generational allocation for returns, global/caller-owned stores, imports, dynamic calls, recursion, exceptions, pinning, or unknown class shapes.

The local ownership graph follows aliases, owned fields (including nested paths), and local Array / StaticArray elements introduced by push, unshift, set, or indexed assignment. It also recognizes direct indexed paths such as values[i].next once the collection owns its elements. Reading an owned field or element into a local creates a shared borrow. Replacing that field, replacing an indexed element, or mutating its collection while the borrow has a later use falls back to the generational path; mutation after the last local use remains eligible.

Direct helper calls carry a composable no-escape and mutation summary: a helper may store one parameter through another parameter's field, or mutate a parameter collection with indexed assignment, push, pop, shift, unshift, or set, only when an arena-valued argument is paired with an arena-local destination at the call site. These effects compose through direct forwarding helpers and are checked against the caller's live field and element borrows. Calls not covered by that summary remain conservative fallbacks. The analysis is intentionally path-insensitive, so ambiguous control flow can lose arena eligibility but cannot turn an unproven escape into an arena allocation.

This is a hybrid allocation path, not a replacement for the generational collector: proven function-local values use arena chunks, while escaping or unknown values remain young/old generational objects. Both paths cooperate at minor-collection boundaries, which also drain completed cached arena chunks.

The old-generation garbage budget defaults to 16 * ASC_GC_GRANULARITY (1 MiB with the default 64 KiB nursery). Set ASC_GC_MAJOR_GRANULARITY to trade peak retained memory for less frequent major pauses.

The transform stamps a wago.asgc ABI marker. Its parallel-mark mode is reserved for modules built with the companion wago-asgc-plugin; serial mode requires no host imports.

For the experimental Wago parallel marker, compile a distinct artifact:

WAGO_ASGC_MODE=parallel-mark asc assembly/index.ts \
  --runtime ./node_modules/@wago/as-stw-gc/runtime/index-parallel \
  --transform ./node_modules/@wago/as-stw-gc/transform/index.mjs \
  --use ASC_WAGO_PARALLEL=1

This imports wago_asgc.mark(fromSpace, toSpace, heapBase) -> i32. The import is synchronous: guest execution is stopped while Wago's bounded worker pool marks the immutable graph. A missing, incompatible, or declining plugin returns skipped, and the runtime performs the same collection serially.

For allocation-heavy workloads with short-lived allocation epochs, the page-recycling runtime is the high-throughput variant:

WAGO_ASGC_MODE=parallel-mark asc assembly/index.ts \
  --runtime ./node_modules/@wago/as-stw-gc/runtime/index-region-parallel \
  --transform ./node_modules/@wago/as-stw-gc/transform/index.mjs \
  --use ASC_WAGO_PARALLEL=1

It allocates managed objects in 64 KiB regions and recycles an all-dead region in O(1), avoiding one TLSF free per object. It requires the Wago plugin: its serial fallback preserves objects but intentionally does not provide the same region-reclamation performance. Individual allocations must fit in a region (less than 64 KiB including runtime overhead). This runtime is experimental: the general index-parallel runtime remains the supported choice for mixed long-lived and short-lived object populations.

Limits: wasm32 only, no shared memory, non-moving objects, and only type layouts described by the transform are eligible for host marking. The plugin refuses unknown/dynamic library visitors rather than guessing their layout.

Verification

npm test
ASGC_ASSEMBLYSCRIPT_ROOT=/path/to/project-with-assemblyscript node test/compile.mjs

Reproducible benchmarks

bench.mjs reports raw wall-clock samples, median, p95/p99, and peak Wasm pages for incremental, frozen legacy Tenure, the current hybrid, and (when enabled) direct-page hybrid. It measures only guest work; fresh instantiation is deliberately excluded from each sample.

TENURE_ASSEMBLYSCRIPT_ROOT=/path/to/json-as npm run bench
TENURE_ASSEMBLYSCRIPT_ROOT=/path/to/json-as \
  TENURE_BENCH_MODE=steady TENURE_BENCH_OUTPUT=tenure-steady.json npm run bench
TENURE_ASSEMBLYSCRIPT_ROOT=/path/to/json-as \
  TENURE_BENCH_MODE=pressure TENURE_BENCH_LOW_MEMORY=1 \
  TENURE_BENCH_LOW_MEMORY_LIMIT=65536 \
  TENURE_BENCH_LINEAR_PAGES=8 npm run bench
TENURE_ASSEMBLYSCRIPT_ROOT=/path/to/json-as \
  TENURE_BENCH_CORPUS=obj-medium TENURE_BENCH_COUNT=10 npm run bench

Modes are fresh (new instance per sample), steady (one warmed instance), and pressure (one warmed instance driven with TENURE_BENCH_PRESSURE_COUNT, so the workload's normal allocation/collection path is measured). TENURE_BENCH_LOW_MEMORY=1 requires an explicit byte cap in TENURE_BENCH_LOW_MEMORY_LIMIT. Choose a cap that accommodates the selected corpus's static data as well as its live heap; the benchmark records it in the output artifact. TENURE_BENCH_CORPUS selects an ephemeral JSON.Obj source from json-as: obj-small (Lottie), obj-medium (CITM catalog), or obj-large (Canada). The generated source is removed after compilation, and its payload name—not a machine-specific temporary path—is retained in the benchmark artifact. TENURE_BENCH_OUTPUT retains the full JSON artifact; record engine flags and the emitted environment block with any performance claim. Direct-page samples also include reserved direct pages and page-cap fallback refills. When ABI-2 barriers are present, samples additionally include dirty-card count, card-table bytes, and safe whole-parent fallback count.

Design work

Clean-slate design and implementation plan documents the lessons from the current implementation and the proposed typed, CFG/SSA-based ownership, borrow, region, and collector redesign.

SIMD-first region design explains how direct region pages and vectorized root/card metadata fit together without pretending that arbitrary object graph traversal is vectorizable.

Unified end-to-end execution plan sequences the compiler, runtime, verifier, SIMD, benchmarking, and release work into one set of deliverables and acceptance gates.