Skip to content

share one run tasks body across install callbacks - #39516

Closed
alii wants to merge 4 commits into
mainfrom
ali/size-install-run-tasks
Closed

alii wants to merge 4 commits into
mainfrom
ali/size-install-run-tasks

Conversation

@alii

@alii alii commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

bun install's run_tasks loop is generic over a callbacks trait, so the whole 20 KB task-drain body was compiled once per caller: 5 copies in bun_install and 2 in bun_jsc, and install_package_with_name_and_resolution was further stamped per pair of const bool flags. The bodies are the same code with different callback targets.

The loop now takes the callbacks as a &mut dyn object and the two verify flags are plain arguments, so one body exists. Pre-LTO the rlib text drops by about 105 KB (run_tasks −108 KB and the flag pairs −44 KB, minus the shared bodies). The dyn calls sit at the per-task boundary (one per finished network or extract task), not inside any per-byte or per-file loop; the extract and manifest paths themselves are unchanged.

Behavior is unchanged. bun-install, bun-add, streaming-extract, tarball-integrity and the workspace tests pass on the debug build.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/isolated-install.test.ts

@coderabbitai

coderabbitai Bot commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b30d9cf1-a548-4d15-a0b1-bdc32ed632ed

📥 Commits

Reviewing files that changed from the base of the PR and between 54784dc and 3d9bb2f.

📒 Files selected for processing (8)
  • src/install/PackageInstaller.rs
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • src/install/PackageManager/PopulateManifestCache.rs
  • src/install/PackageManager/install_with_manager.rs
  • src/install/PackageManager/runTasks.rs
  • src/install/hoisted_install.rs
  • src/install/isolated_install.rs
  • src/jsc/AsyncModule.rs

Included review availability: Your plan includes up to 5 reviews per rolling hour; 0 remain after this review.


Walkthrough

The task runner now uses lifetime-parameterized callback trait objects and runtime flags. Package, store, manifest, hoisted, isolated, and queue installation paths were updated to pass callback instances directly.

Changes

Runtime callback dispatch migration

Layer / File(s) Summary
Task runner callback contract and dispatch
src/install/PackageManager/runTasks.rs
RunTasksFlags and RunTasksCallbacks<'a> replace generic callback contexts and associated constants. Task, download, extraction, Git, and resolution hooks use runtime flags and dynamic dispatch.
Installer callback implementations and package state flags
src/install/PackageInstaller.rs, src/install/hoisted_install.rs, src/install/isolated_install.rs
Installation verification and pending-install state use runtime booleans. Hoisted and isolated installers implement the callback trait directly and call run_tasks without wrapper types.
Package manager callback callers
src/install/PackageManager/PackageManagerEnqueue.rs, src/install/PackageManager/PopulateManifestCache.rs, src/install/PackageManager/install_with_manager.rs
Callback implementations return RunTasksFlags, and task runner calls pass callback instances directly.
Async queue callback integration
src/jsc/AsyncModule.rs
Queue callbacks store mutable queue state and progress configuration. Queue task execution uses one runtime-selected progress path.

Possibly related PRs

  • oven-sh/bun#38180: Related runtime auto-install task and callback plumbing in package manager enqueue and async module code.

Suggested reviewers: jarred-sumner, robobun

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: sharing one compiled run_tasks body across install callbacks.
Description check ✅ Passed The description explains the implementation, performance impact, behavior, and verification results, although it does not use the template headings.

Comment @coderabbitai help to get the list of available commands.

@alii

alii commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator

Closed as superseded by #39770, which landed the same change on main (run_tasks compiled once through an ErasedCallbacks table, the two bools as runtime arguments). Reopen or ask if you want the dyn trait version as a follow up cleanup on top of main. The test from 01efdf5 applies cleanly to main on its own.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it refactors the core run_tasks install loop across 8 files — swapping static dispatch for &mut dyn, reworking the trait's lifetime parameter, and touching the raw-pointer provenance setup around the scopeguard shadows — a human look would still be worthwhile.

What was reviewed:

  • Every C::CONST → flags.field and C::method(ctx, …) → extract_ctx.method(…) site in run_tasks maps 1:1; flags is snapshotted once before the extract_ctx_ptr shadow.
  • The new as_package_installer/as_store_installer return self directly, removing the old unsafe lifetime-narrowing casts.
  • process_dependency_list_for_ctx now calls extract_ctx.flags() after deriving ctx_ptr — examined under Stacked Borrows and ruled out as a regression (shared read, and the closure runs after the borrow ends).
  • All six impls (PackageInstaller, Store::Installer, QueueRunTasksCallbacks, and the three unit markers) set the same flag combinations their old associated consts did.
Extended reasoning...

Overview

This PR de-monomorphizes bun install's run_tasks loop and install_package_with_name_and_resolution. The RunTasksCallbacks trait moves from an associated-const / associated-type design (with type Ctx and const HAS_* gates) to a dyn-compatible trait whose impls are the context types (PackageInstaller<'a>, Store::Installer<'a>, a QueueRunTasksCallbacks wrapper, and three unit markers). A RunTasksFlags struct returned from flags() replaces the associated consts, and run_tasks takes &mut dyn RunTasksCallbacks<'_> with #[inline(never)]. Separately, the two const-bool generics on install_package_with_name_and_resolution and fail_with_invalid_url become runtime bool parameters. The stated goal is ~105 KB of pre-LTO text savings by collapsing 7 stamped copies of the 20 KB body into one.

Security risks

None. This is a dispatch-mechanism refactor with no change to input handling, validation, network/registry paths, or filesystem operations. The extract, manifest, and integrity paths are untouched.

Level of scrutiny

High. run_tasks is the main-thread drain loop for every network/extract/git task during bun install, and it is dense with unsafe (raw-pointer provenance roots for scopeguard, union arm accesses, detached lifetimes). The transformation itself is mechanical — I traced each C::HAS_* / C::IS_* gate to its flags.* replacement and each static C::method(ctx, …) call to its extract_ctx.method(…) equivalent, and they all line up. The flags snapshot is taken from the fn-param extract_ctx before the extract_ctx_ptr provenance root is derived and the param is shadowed, so the guard's capture of flags.progress_bar is by value and unaffected by the reborrow dance. The trait's new 'a lifetime parameter lets as_package_installer/as_store_installer return &mut PackageInstaller<'a> / &mut Store::Installer<'a> as plain self, deleting two unsafe variance casts — a net safety improvement.

Other factors

Two candidate Stacked Borrows issues around process_dependency_list_for_ctx (calling extract_ctx.flags() after deriving ctx_ptr) were raised by finder agents and refuted by verifiers; flags() takes &self and the closure that dereferences ctx_ptr runs only after that shared reborrow is dead, matching the surrounding provenance conventions in this file. No new tests ship, which is reasonable for a behavior-preserving refactor covered by the existing install/add/streaming-extract/integrity/workspace suites the description names. Given the breadth (8 files, core install path) and the unsafe surface it threads through, I'm deferring rather than approving so a maintainer can confirm the code-size win and the dyn-dispatch placement (per-task boundary) matches their perf expectations.

process_dependency_list already takes a ctx value for its on_resolve
callback, so process_dependency_list_for_ctx can hand it the &mut dyn
directly instead of smuggling a raw pointer into the closure.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. The const-generic → runtime-bool and static → dyn-trait conversions look mechanically correct and drop two unsafe lifetime casts. Because it restructures the trait lifetime on RunTasksCallbacks and threads a *mut dyn through the existing raw-pointer provenance dance in run_tasks, a human look at the install core loop would still be worthwhile.

What was reviewed:

  • All run_tasks / install_package_with_name_and_resolution / fail_with_invalid_url call sites updated; no stragglers.
  • flags() snapshot at run_tasks entry vs. per-call in process_dependency_list_for_ctx — every impl returns a fixed value, so no divergence from the old associated-const behavior.
  • process_dependency_list_for_ctx now passes extract_ctx as the ctx argument instead of smuggling a raw pointer through a closure — matches process_dependency_list's FnOnce(C) shape.
  • as_package_installer/as_store_installer now return self with the trait's 'a, replacing the raw-pointer variance cast.
Extended reasoning...

Overview

The PR de-monomorphizes two heavily-stamped generics in bun install: run_tasks (previously generic over a RunTasksCallbacks trait with an associated Ctx type and const bool gates, instantiated 7×) and install_package_with_name_and_resolution (previously stamped per <const NEEDS_VERIFY, const IS_PENDING_PACKAGE_INSTALL> pair). The trait is reworked to be dyn-compatible — hooks become &mut self methods, the const gates become a RunTasksFlags struct returned from flags(), and the two installer downcasts return &mut PackageInstaller<'a> / &mut Store::Installer<'a> with 'a as a trait lifetime parameter. run_tasks now takes &mut dyn RunTasksCallbacks<'_> and is #[inline(never)]. Eight files across src/install/ and src/jsc/AsyncModule.rs update to the new shape.

Security risks

None. This is an internal dispatch refactor with no change to input handling, validation, or trust boundaries.

Level of scrutiny

Medium-high. The change is mechanical and behavior-preserving in intent, but it sits inside run_tasks, which is the install task-drain core and already relies on carefully-staged raw-pointer provenance (manager_ptr / extract_ctx_ptr shadowing for the drain scopeguard). The PR changes extract_ctx_ptr to a fat *mut dyn and reworks how the trait's inner lifetime flows through as_*_installer. I traced each hook site and confirmed the flag gates match the old HAS_* / IS_* consts one-for-one, and that every impl's flags() is effectively constant (Queue's progress is fixed at construction), so snapshotting once at entry matches the old semantics. I also confirmed there are no remaining callers of the old generic signatures.

Other factors

Two unsafe blocks (the inner-lifetime variance casts in hoisted_install.rs and isolated_install.rs) are removed outright, and process_dependency_list_for_ctx drops its raw-pointer closure smuggle in favor of passing the callbacks object as the generic ctx — both are net safety wins. robobun reports the full install test matrix (isolated/hoisted, patch, git-deps, streaming-extract, tarball-integrity, workspaces, auto-install) passing on the debug build. That said, this is a ~350-line refactor of production-critical native code with a stated perf/size rationale; per the repo's review norms around refactors and memory-safety-adjacent code, a maintainer familiar with the install subsystem should sign off on the dyn-dispatch trade-off and the RunTasksCallbacks<'a> lifetime shape.

The other three callers of install_package_with_name_and_resolution
already label needs_verify and is_pending_package_install at the call
site. This one passed two bare literals.
@robobun

robobun commented Aug 18, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 2:41 AM PT - Aug 18th, 2026

✅ @robobun, your commit 01efdf5e5beab982265a49198c28852705f04914 passed in Build #100514! 🎉


🧪   To try this PR locally:

bunx bun-pr 39516

That installs a local version of the PR into your bun-39516 executable, so you can run:

bun-39516 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. It's a mechanical de-monomorphization of run_tasks and install_package_with_name_and_resolution, but it redesigns the callbacks trait (new 'a lifetime parameter, &mut dyn dispatch, *mut dyn fat pointer in the scopeguard provenance dance) across the install core loop, so a maintainer glance at the trait shape would still be worthwhile.

What was reviewed:

  • All six RunTasksCallbacks impls return the same flag values as the old associated consts; flags() snapshotted once at the top of run_tasks is equivalent since every impl returns a constant.
  • process_dependency_list<C> accepts C = &mut dyn RunTasksCallbacks and only threads ctx through to on_resolve, so dropping the raw-pointer capture in process_dependency_list_for_ctx is sound.
  • The two unsafe lifetime-narrowing casts in as_package_installer/as_store_installer are replaced by safe identity returns via the new trait lifetime; the deleted HoistedRunTasksCallbacks/StoreRunTasksCallbacks phantom types have no remaining references.
  • All four callers of install_package_with_name_and_resolution (and fail_with_invalid_url) pass the same bool values as the old const generics.
Extended reasoning...

Overview

This PR de-monomorphizes two hot install-path functions to shrink binary size (~105 KB pre-LTO):

  • run_tasks in runTasks.rs — was generic over RunTasksCallbacks with an associated Ctx type and HAS_*/IS_* associated consts; now takes &mut dyn RunTasksCallbacks<'_> and reads a RunTasksFlags struct once via flags(). Tagged #[inline(never)].
  • install_package_with_name_and_resolution in PackageInstaller.rs — two const-generic bools become plain runtime args.

Six impls updated: three unit-marker types (VoidRunTasksCallbacks, ManifestsOnlyCallbacks, InstallWaitCallbacks), PackageInstaller<'a> and store::Installer<'a> now impl the trait directly (phantom wrapper types deleted), and QueueRunTasksCallbacks becomes a struct wrapping &mut Queue + a runtime progress bool (was <const PROGRESS: bool>). Eight files touched, ~400 lines of diff, all in src/install/ and src/jsc/AsyncModule.rs.

Security risks

None. This is an internal dispatch-mechanism refactor with no new inputs, no parsing, no auth/crypto/network surface changes. Net unsafe is reduced: two unsafe lifetime-narrowing pointer casts and one raw-pointer closure capture are removed.

Level of scrutiny

Medium-high. The change is mechanical and behavior-preserving by construction, but it sits in bun install's core task-drain loop — production-critical, dense with raw-pointer provenance handling and scopeguard defers, and the repo's review guidance calls native memory safety the most-blocked category. The trait gains a lifetime parameter and run_tasks now stores a *mut dyn fat pointer for its scopeguard; both are straightforward but worth a maintainer's eye given how load-bearing the surrounding provenance comments are.

Other factors

  • No CODEOWNERS on src/install/.
  • The bot ran the full install/isolated/patch/git-deps/workspace/auto-install suites on the debug build and they pass; the 14 bun-install.test.ts failures are external-host tests that fail identically on main.
  • process_dependency_list's signature (ctx: C, on_resolve: Option<impl FnOnce(C)>) was verified to accept C = &mut dyn RunTasksCallbacks and only forwards ctx to the callback, so the removed raw-pointer capture was genuinely unnecessary.
  • The QueueRunTasksCallbacks refactor in AsyncModule.rs preserves the progress → LogLevel mapping exactly (true → Default, false → DefaultNoProgress).

I'm deferring rather than approving because the trait redesign (new lifetime parameter, dyn-compatibility) and the fat-pointer scopeguard change touch enough of the install core that a maintainer confirming the shape is warranted, even though every substitution I traced is 1:1.

Nothing asserted the isolated installer's on_package_download_error path
before. With a lockfile the store installer downloads the tarball itself,
and a 404 has to be reported as its own "failed to download" error (the
hoisted installer and the resolution phase log "GET <url> - 404"). This
pins the download error dispatch that run_tasks now selects through the
flags struct.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it reshapes the core run_tasks drain loop — swapping const-generic dispatch for &mut dyn, threading a new 'a lifetime through the trait's downcasts, and changing the fat-pointer provenance root at extract_ctx_ptr — a human look would still be worthwhile.

What was reviewed:

  • Each flags() impl matches its former associated consts one-for-one; flags is read once at loop entry and captured by the drain defer!, so gate values stay consistent per call.
  • process_dependency_list_for_ctx now passes extract_ctx as the generic ctx arg — process_dependency_list only touches ctx in the trailing on_resolve(ctx), so the dropped raw-pointer capture is safe.
  • The as_package_installer/as_store_installer identity impls return &mut Self<'a> directly, replacing the old unsafe inner-lifetime narrowing casts; no remaining references to the removed HoistedRunTasksCallbacks/StoreRunTasksCallbacks markers.
  • New isolated-install test uses a local Bun.serve and tempDir, and asserts the store installer's own error text (distinct from the hoisted GET … - 404 path).
Extended reasoning...

Overview

This PR de-monomorphizes bun install's run_tasks loop and install_package_with_name_and_resolution. The RunTasksCallbacks trait's associated consts (HAS_ON_*, IS_*_INSTALLER, PROGRESS_BAR, MANIFESTS_ONLY) and type Ctx become a RunTasksFlags struct returned by fn flags(&self), and the trait is now dyn-compatible with a 'a lifetime for the two installer downcasts. All six impls (PackageInstaller, Store::Installer, VoidRunTasksCallbacks, ManifestsOnlyCallbacks, InstallWaitCallbacks, QueueRunTasksCallbacks) are updated in place, the two phantom marker types are deleted, and install_package_with_name_and_resolution's two const-bool generics become plain arguments. A new test covers the store installer's tarball-404 callback path.

Security risks

None. No user-input parsing, auth, crypto, or trust boundaries are touched; the change is dispatch mechanics only.

Level of scrutiny

High. run_tasks is the hot loop that drains every network/extract/git task during bun install, and the surrounding code is dense with raw-pointer provenance roots (manager_ptr, extract_ctx_ptr) and scopeguard::defer! reborrows. The refactor changes extract_ctx_ptr from a thin *mut C::Ctx to a fat *mut dyn RunTasksCallbacks<'_> and reshapes how the two as_* downcasts hand back &mut Installer<'a>. I traced each flag gate and callback dispatch site against its previous const-gate and found them equivalent, and the new lifetime plumbing lets the identity casts drop their unsafe — but this is exactly the kind of change a maintainer should eyeball.

Other factors

  • The adopt comment reports 13+ install test suites passing on the debug build; the 14 bun-install.test.ts failures are external-host tests that also fail on main.
  • The added test follows harness conventions (local Bun.serve({ port: 0 }), tempDir, runBunInstall helper, per-test cache dir) and asserts the store-installer-specific error string, so it exercises on_package_download_error_store rather than the generic log path.
  • process_dependency_list's generic C is only consumed by the trailing on_resolve(ctx) call, so passing the &mut dyn through as ctx (instead of the previous raw-pointer closure capture) is a straightforward simplification.
  • The dyn-dispatch cost sits at the per-completed-task boundary, not inside any per-byte or per-file loop, matching the PR description; whether that trade-off against ~105 KB of duplicated text is desirable is a maintainer call.

@robobun

robobun commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

@alii this branch now conflicts with main because #39770 (merged Aug 21) landed the same change by another route. On main, run_tasks is compiled once: run_tasks<C> is a small shim that erases the context to *mut () and calls a non generic run_tasks_erased with an ErasedCallbacks table of flags and fn pointers (runTasks.rs). The two const bools on install_package_with_name_and_resolution are runtime bools on main as well. So the size win this PR was opened for is already in.

What is left of this branch, if rebased, is a redesign of that erasure layer: the callbacks become a dyn compatible trait that the installers implement themselves, which deletes ErasedCallbacks, the *mut () casts and the two unsafe installer recoveries. Every hunk of the base commit conflicts, so that is a rewrite against main's code, not a rebase. I do not want to make that call on a change Jarred merged three days ago, so I have not pushed it.

Two options:

  1. Close this PR as superseded by Trim ~2 MB from the release binary without touching hot paths #39770.
  2. I rewrite it on top of main as a follow up cleanup (dyn trait instead of the fn pointer table).

The test from 01efdf5 (a tarball 404 during an install from a lockfile has to come out as the store installer's own error) applies cleanly to main and is worth keeping either way. Tell me which option you want.

@robobun

robobun commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Closing as superseded by #39770, which compiles run_tasks once (ErasedCallbacks table in runTasks.rs) and makes the same two bools runtime arguments. Every hunk of this branch conflicts with that code.

If you want the dyn trait version as a follow up cleanup of the erasure layer, reopen this or say so and I will redo it on top of main. The test from 01efdf5 (a tarball 404 during an install from a lockfile has to come out as the store installer's own "failed to download" error) applies cleanly to main on its own.

@robobun robobun closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants