Skip to content

perf: move provider to background thread - #1486

Open
Wodann wants to merge 5 commits into
mainfrom
refactor/provider-on-background-thread
Open

perf: move provider to background thread#1486
Wodann wants to merge 5 commits into
mainfrom
refactor/provider-on-background-thread

Conversation

@Wodann

@Wodann Wodann commented Jun 18, 2026

Copy link
Copy Markdown
Member

Replaces the synchronisation mechanism of the JSON-RPC provider from mutexes and tokio blocking threads to channels and a dedicated OS thread. Requests and interval mining are serialised on the thread that owns the ProviderData, without any locking.

Design

The napi handleRequest method deserialises the request on the JS thread (microseconds), enqueues it on the provider's channel, and returns a promise. The provider's background thread executes the request and settles the promise directly — a JsDeferred can be settled from any thread. That makes a request cost two thread handoffs (JS → provider thread → JS), fewer than main, which paid for a spawn_blocking round trip on every request.

Consequences:

  • The blocking Provider::handle_request is reimplemented on top of the callback path and keeps its public API (used by tests).
  • On shutdown, the background thread drains still-queued requests and settles their promises with the new ProviderError::UnexpectedTermination, so JS promises never dangle.
  • Response conversion (cast_response) now runs on the background thread; request deserialisation runs on the JS thread.

Considered designs

The naive design — send over a channel, block a tokio blocking thread on the reply — regressed the provider benchmark ~15% on request-dense scenarios: four thread handoffs per request (+2.3 context switches, +4.8 futex syscalls, a fixed ~19 µs). Three fixes were implemented and benchmarked locally (rocketpool scenario, median of 5 runs, perf stat for the counters):

design wall time context switches futex syscalls
channels + blocking recv() (this PR, before fix) 31.2 s 1.94 M 3.40 M
merge-base (mutex + blocking threads) 23.8 s 1.45 M 2.37 M
in-place execution under parking_lot::Mutex; thread only for interval mining 22.8 s 1.43 M 2.41 M
napi awaits a tokio oneshot completed by the provider thread 21.8 s 1.37 M 2.28 M
settle the JS promise from the provider thread (chosen) 12.0 s 0.47 M 0.49 M

The mutex variant proved the regression was purely the per-request channel round trip, but reintroduces the locking this PR set out to remove. The oneshot variant fixes the regression but keeps the blocking-pool hop. Settling the promise from the provider thread removes that hop too — it turns out to be roughly half of the provider benchmark's wall time: the full local suite drops from 442 s (merge-base) to 220 s.

Benchmark results

JS scenario runner benchmark (CI, vs latest main):

scenario main PR change
neptune-mutual-blue-protocol 20.41 s 16.62 s −18.6%
openzeppelin-contracts 8.82 s 7.00 s −20.6%
rocketpool 12.12 s 8.90 s −26.6%
safe-contracts 0.66 s 0.49 s −25.9%
seaport 4.74 s 3.95 s −16.7%
synthetix 154.83 s 112.36 s −27.4%
uniswap-v3-core 3.82 s 3.14 s −17.8%
All scenarios 205.39 s 152.45 s −25.8%

HH3 regression benchmark (CI, vs latest main):

benchmark main PR change
openzeppelin-contracts / test mocha 64.3 s 57.5 s −10.5%
lidofinance-core / test mocha 10.8 s 10.7 s −0.8%
ens-contracts / test vitest 4.22 s 4.11 s −2.5%

The scenario runner improves more than HH3 because it replays only the JSON-RPC request stream against the provider — it isolates exactly the slice this PR changes. HH3 wall-clock time also includes the test runner, the test suite's own JS, client libraries and process startup, so the removed per-request synchronisation cost is diluted; how much a suite improves tracks how much of its runtime is spent in provider calls.

Validation

  • The scenario runner replays ~3.4 M captured requests across 7 scenarios; the replayed failure indices are identical to main in every scenario, locally and in CI.
  • cargo test -p edr_provider --features test-utils: 247 tests pass, including the interval mining integration tests.
  • The HH3 regression benchmark workflow passed.
  • perf stat context-switch and futex counts (table above) confirm the thread handoffs are actually gone, not just faster.

@Wodann Wodann added the no changeset needed This PR doesn't require a changeset label Jun 18, 2026
@Wodann
Wodann temporarily deployed to github-action-benchmark June 18, 2026 03:28 — with GitHub Actions Inactive
@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 2996ae3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Wodann
Wodann had a problem deploying to github-action-benchmark June 18, 2026 03:30 — with GitHub Actions Failure
@Wodann
Wodann temporarily deployed to github-action-benchmark June 18, 2026 03:30 — with GitHub Actions Inactive
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 59.82143% with 180 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.93%. Comparing base (a69221b) to head (2996ae3).

Files with missing lines Patch % Lines
crates/edr_provider/src/backend.rs 54.30% 135 Missing and 3 partials ⚠️
crates/edr_napi/src/provider.rs 57.14% 11 Missing and 1 partial ⚠️
crates/edr_provider/src/provider.rs 79.24% 10 Missing and 1 partial ⚠️
crates/edr_napi_core/src/provider.rs 78.26% 7 Missing and 3 partials ⚠️
crates/edr_napi/src/mock.rs 0.00% 8 Missing ⚠️
crates/edr_provider/src/error.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1486      +/-   ##
==========================================
- Coverage   79.89%   75.93%   -3.96%     
==========================================
  Files         452      452              
  Lines       78956    79068     +112     
  Branches    78956    79068     +112     
==========================================
- Hits        63078    60043    -3035     
- Misses      13697    16942    +3245     
+ Partials     2181     2083      -98     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Base automatically changed from refactor/cancellable-thread to main June 18, 2026 15:04
@Wodann
Wodann force-pushed the refactor/provider-on-background-thread branch from 65b08b7 to 3f812fe Compare June 18, 2026 18:56
@Wodann
Wodann had a problem deploying to github-action-benchmark June 18, 2026 18:56 — with GitHub Actions Error
@Wodann
Wodann force-pushed the refactor/provider-on-background-thread branch from 3f812fe to c689632 Compare June 18, 2026 18:57
@Wodann
Wodann temporarily deployed to github-action-benchmark June 18, 2026 18:57 — with GitHub Actions Inactive
@Wodann

Wodann commented Jun 18, 2026

Copy link
Copy Markdown
Member Author

/bench

@Wodann
Wodann had a problem deploying to github-action-benchmark June 18, 2026 19:00 — with GitHub Actions Failure
@Wodann
Wodann temporarily deployed to github-action-benchmark June 18, 2026 19:00 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark June 19, 2026 14:02 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark June 19, 2026 14:04 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark June 19, 2026 14:04 — with GitHub Actions Inactive
@Wodann

Wodann commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

/bench

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for 30ee8a5c9bcf against Hardhat main.

@Wodann

Wodann commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

/bench

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for 30ee8a5c9bcf against Hardhat main.

@Wodann
Wodann temporarily deployed to github-action-benchmark June 26, 2026 14:39 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark June 26, 2026 14:41 — with GitHub Actions Inactive
@Wodann
Wodann had a problem deploying to github-action-benchmark June 26, 2026 14:41 — with GitHub Actions Failure
@Wodann

Wodann commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

/bench hardhat-ref=ci/fix-outdated-deps

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for 939462763c4d against Hardhat ci/fix-outdated-deps.

@Wodann

Wodann commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

/bench hardhat-ref=ci/fix-outdated-deps

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for 939462763c4d against Hardhat ci/fix-outdated-deps.

@Wodann

Wodann commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

/bench

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for 939462763c4d against Hardhat main.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Regression benchmark passed for 939462763c4d against Hardhat main.

View workflow run

@Wodann
Wodann temporarily deployed to github-action-benchmark August 6, 2026 16:53 — with GitHub Actions Inactive
@Wodann
Wodann had a problem deploying to github-action-benchmark August 6, 2026 17:27 — with GitHub Actions Failure
@Wodann
Wodann had a problem deploying to github-action-benchmark August 6, 2026 17:27 — with GitHub Actions Failure
Replace the per-request `spawn_blocking` + blocking channel round trip
with a completion callback: `handle_request` now deserializes the
request on the calling thread, enqueues it on the provider's background
thread, and settles the napi deferred from that thread once the
response is available. This cuts the thread handoffs per request from
four to two (~7x fewer futex syscalls), making the provider benchmark
suite ~2x faster than main.

Queued requests are settled with the new
`ProviderError::UnexpectedTermination` when the provider shuts down, so
pending promises always resolve.
@Wodann

Wodann commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

/bench

@Wodann
Wodann requested a review from Copilot August 6, 2026 23:05
@Wodann Wodann self-assigned this Aug 6, 2026
@Wodann
Wodann marked this pull request as ready for review August 6, 2026 23:08

Copilot AI 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.

Pull request overview

Refactors edr_provider to run all JSON-RPC request handling (and interval mining) on a dedicated background OS thread, replacing mutex-based synchronization and tokio blocking with channel-based message passing. This aims to improve safety/latency on callers (notably the JS main thread) by ensuring requests are serialized off-thread and surfaced via callbacks.

Changes:

  • Introduce a new backend event loop running on a dedicated CancellableThread, processing requests via crossbeam-channel and integrating interval mining scheduling.
  • Update provider APIs and N-API bindings to enqueue requests and resolve responses asynchronously (callback/promise-based) instead of blocking execution on the JS thread.
  • Add integration coverage for interval mining enable/disable behavior and adjust trait bounds for Send requirements.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/edr_provider/tests/integration/mod.rs Registers the new interval mining integration test module.
crates/edr_provider/tests/integration/interval_mining.rs Adds integration tests for interval mining and evm_setIntervalMining behavior.
crates/edr_provider/src/spec.rs Tightens SyncProviderSpec bounds to ensure RPC request types are Send.
crates/edr_provider/src/requests/eth/mine.rs Refactors interval mining config update to mutate ProviderData directly.
crates/edr_provider/src/provider.rs Reworks Provider to send requests over channels to a background thread; adds enqueue API.
crates/edr_provider/src/lib.rs Wires in the new backend module; removes old interval module.
crates/edr_provider/src/interval.rs Removes the prior tokio-task-based IntervalMiner implementation.
crates/edr_provider/src/error.rs Adds ProviderError::UnexpectedTermination and maps it to INTERNAL_ERROR.
crates/edr_provider/src/data.rs Stores interval mining config in ProviderData and exposes getter/setter.
crates/edr_provider/src/config.rs Derives Eq/PartialEq for IntervalConfig to support reschedule detection.
crates/edr_provider/src/backend.rs New background-thread event loop handling requests and interval mining scheduling.
crates/edr_provider/Cargo.toml Adds crossbeam-channel and edr_utils_sync dependencies.
crates/edr_napi/src/provider.rs Changes handle_request to return a JS Promise resolved via provider enqueue callback; scenario file now shared via Arc.
crates/edr_napi/src/mock.rs Updates mock provider to implement the new enqueue-based trait.
crates/edr_napi_core/src/provider.rs Replaces blocking handle_request with enqueue-based API; refactors failed-deserialization handling.
crates/edr_generic/tests/integration/issues/issue_947.rs Updates trait bounds to satisfy new provider constraints.
crates/edr_generic/tests/integration/helpers.rs Updates helper trait bounds to satisfy new provider constraints.
Cargo.lock Records new dependency additions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +135 to +138
let result = data
.logger_mut()
.print_method_logs(&method_name, Some(&error))
.map_err(ProviderError::Logger);
result,
call_trace_arenas: traces,
})
response_receiver.recv().expect(BACKEND_THREAD_TERMINATED)
Comment on lines +57 to +63
let (response_sender, response_receiver) = bounded(1);
self.request_sender
.send(new_request_fn(response_sender))
.expect(BACKEND_THREAD_TERMINATED);

response_receiver.recv().expect(BACKEND_THREAD_TERMINATED)
}
@Wodann
Wodann temporarily deployed to github-action-benchmark August 6, 2026 23:27 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark August 6, 2026 23:29 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark August 6, 2026 23:29 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for 2996ae337b46 against Hardhat main (benchmarks matching test solidity,test mocha,test vitest).

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ Regression benchmark passed for 2996ae337b46 against Hardhat main.

View workflow run

@Wodann
Wodann requested a review from a team August 7, 2026 01:31

@anaPerezGhiglia anaPerezGhiglia 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.

Really nice! the design direction and the benchmark numbers speak for themselves 🙌

None of my comments is blocking: they're a mix of pre-existing validation holes this design escalates (proposed as a follow-up issue + fix), a hardening suggestion for the event loop's panic path and structure/naming suggestions.
Submitting as "comment" rather than approval only because several of these may turn into changes worth a second look. Happy to approve once the discussions settle!

Comment on lines +269 to +270
/// Configuration for interval mining, if enabled. The provider's background
/// thread owns the interval timer and reads this field to (re)schedule it.

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'm not fan of these type of Claude comments where we detail the internals of the consumer of this information.

Comment on lines +450 to +452
/// Sets the interval mining configuration. The provider's background thread
/// picks up the change after the current request and reschedules its timer
/// accordingly. Passing `None` disables interval mining.

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.

same here. This method does not care about the provider's background thread. I feel these type of comments ends up breaking the encapsulation built on the code, making them super hard to maintain

Comment on lines +18 to +19
/// returning immediately without waiting for the request to be handled, as
/// the caller may be the JS main thread.

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.

returning immediately might be too harsh. Actually the impl 20 lines below does not follow it.

Suggested change
/// returning immediately without waiting for the request to be handled, as
/// the caller may be the JS main thread.
/// as callers (e.g. the JS main thread) rely on this method returning without
/// waiting for the request to be handled.

let mut interval_timer = next_interval_timer(data.interval_config());

loop {
crossbeam_channel::select_biased! {

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.

By design, I agree with having a single background thread that works as an event loop with this prioritization: for any valid interval config, the timer is re-armed only after mining completes, so the request queue always keeps making progress.

Unfortunately, interval mining has some pre-existing validation holes that compromise the whole provider under this design, since the thread that mines is now the same one that serves requests (both reproduced on this branch):

  • Range { min: 0, max: 0 } is accepted on both entry points. generate_interval() returns 0 on every draw, so the timer is immediately ready on every re-arm and select_biased! always picks mining: requests are never serviced and their JS promises never settle, while the loop hot-mines blocks. On main, the same config busy-mines but requests still progress through the mutex.
  • Range { min > max } is rejected on the RPC path but not on the napi initial-config path; random_range panics on the empty range when the timer is armed. On main that panic killed the detached mining task and the provider kept serving requests; here it kills the background thread that owns ProviderData, leaving the provider unusable.

Neither hole is introduced by this PR and both are edge cases, so I don't think this should block it. But since this design escalates them from "noisy/degraded" to "provider compromised", I'd address them as a separate issue + fix shortly after this lands. The fix is contained: normalize/reject at the two conversion points, mirroring the existing "zero implies disabled" scalar semantics. The event loop itself should need no changes.

Comment on lines +154 to +158
while let Ok(message) = request_receiver.try_recv() {
if let BackendRequest::Request { on_response, .. } = message {
on_response(Err(ProviderError::UnexpectedTermination));
}
}

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.

On a background-thread panic this drain loop is skipped (the thread unwinds out of it), so an already-queued request is dropped unsettled: its JS promise dangles forever, and the blocking handle_request panics on the caller's thread via .expect(BACKEND_THREAD_TERMINATED).
I reproduced this locally with an interval mining config of min > max (see my other comment). Claude suggested moving the drain into a drop guard, which I tested locally and works, but I haven't spent time investigating other alternatives.

recv(cancellation_receiver) -> _ => break,
// Interval mining takes precedence over incoming requests. An overdue
// deadline yields a zero duration, so `after` is immediately ready.
recv(interval_timer) -> _ => {

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 think we should reassess what valid values for the mining interval are: with a valid-but-very-small interval, request handling can get throttled because the event loop is "too busy" mining blocks. To users this would surface as EDR taking long to resolve requests.

Comment on lines +162 to +215
fn execute_request<ChainSpecT, TimerT>(
data: &mut ProviderData<ChainSpecT, TimerT>,
request: ProviderRequest<ChainSpecT>,
) -> Result<ResponseWithCallTraces, ProviderErrorForChainSpec<ChainSpecT>>
where
ChainSpecT: SyncProviderSpec<
TimerT,
PooledTransaction: IsEip155,
SignedTransaction: Default
+ TransactionMut
+ TransactionType<Type: IsEip4844>
+ TransactionValidation<ValidationError: PartialEq>,
>,
TimerT: Clone + TimeSinceEpoch,
{
match request {
ProviderRequest::Single(request) => execute_single_request(data, *request),
ProviderRequest::Batch(requests) => execute_batch_request(data, requests),
}
}

/// Executes a batch of JSON requests for an execution provider.
fn execute_batch_request<ChainSpecT, TimerT>(
data: &mut ProviderData<ChainSpecT, TimerT>,
request: Vec<MethodInvocation<ChainSpecT>>,
) -> Result<ResponseWithCallTraces, ProviderErrorForChainSpec<ChainSpecT>>
where
ChainSpecT: SyncProviderSpec<
TimerT,
PooledTransaction: IsEip155,
SignedTransaction: Default
+ TransactionMut
+ TransactionType<Type: IsEip4844>
+ TransactionValidation<ValidationError: PartialEq>,
>,
TimerT: Clone + TimeSinceEpoch,
{
let mut results = Vec::new();
let mut traces = Vec::new();

for req in request {
let response = execute_single_request(data, req)?;
results.push(response.result);
traces.extend(response.call_trace_arenas);
}

let result = serde_json::to_value(results).map_err(ProviderError::Serialization)?;
Ok(ResponseWithCallTraces {
result,
call_trace_arenas: traces,
})
}

fn execute_single_request<ChainSpecT, TimerT>(

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 would have expected this module to own a single responsibility: the event loop. execute_request / execute_batch_request / execute_single_request don't care where they run, they take &mut ProviderData and a request, nothing thread-related.

Concretely, I'd propose moving all three to the requests module: they route MethodInvocation to the handlers that already live there. Optionally, the big match in execute_single_request could be extracted into its own routing function, separating it from the method-logging wrapper around it.

That would leave this module with only the code that is genuinely about scheduling, and make the dispatch match testable as plain routing, with no logging concerns mixed in.

// layout-computation recursion limit of 128.
#![recursion_limit = "256"]

mod backend;

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.

nitpick: I'm not a fan of this name. I feel it's vague and it's not clear what it's responsible for. What do you think about event_loop?
That way we'd have event_loop::run and event_loop::Message instead of BackendRequest (BACKEND_THREAD_TERMINATED const should be renamed too to follow along).

alloy-sol-types.workspace = true
anyhow = { workspace = true, optional = true }
auto_impl = { version = "1.2", default-features = false }
crossbeam-channel = "0.5"

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.

there are already three workspace members using this dependency. Should we define it in [workspace.dependencies] like the other shared external deps?

Comment on lines +157 to +165
let (response_sender, response_receiver) = bounded(1);

for req in request {
let response = self.handle_single_request(data, req)?;
results.push(response.result);
traces.extend(response.call_trace_arenas);
}
self.enqueue_request(
request,
Box::new(move |response| {
// Ignore the error: the caller may have stopped waiting.
let _ = response_sender.send(response);
}),
);

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 think handle_request, send_request_and_wait and enqueue_request are re-implementing pieces of each other, with diverging failure handling: send_request_and_wait panics via .expect(BACKEND_THREAD_TERMINATED) where enqueue_request gracefully settles the callback with UnexpectedTermination. Same failure, two behaviors, three lines apart.

What do you think about splitting this into two building blocks?

  • send_message(message) -> Result<(), BackendRequest>: the raw send, handing the message back on failure. Shared by enqueue_request (which needs the message back to recover its callback) and send_request_and_wait.
  • wait_for_reply(enqueue_fn) -> Result<ResponseT, ProviderError>: creates the bounded(1) reply channel, lets the closure dispatch a message embedding the sender, and maps a disconnected recv to UnexpectedTermination. Shared by handle_request (composed on top of enqueue_request, so the blocking API keeps exercising the exact same path as the napi caller) and send_request_and_wait.

With that, send_request_and_wait doesn't even need explicit error handling.

I tried this locally and the full edr_provider suite stays green, so happy to share the patch if useful.

@Wodann
Wodann had a problem deploying to github-action-benchmark August 11, 2026 14:36 — with GitHub Actions Error
@Wodann
Wodann force-pushed the refactor/provider-on-background-thread branch from a178475 to 2996ae3 Compare August 11, 2026 14:41
@Wodann
Wodann temporarily deployed to github-action-benchmark August 11, 2026 14:41 — with GitHub Actions Inactive
@Wodann Wodann changed the title refactor: move provider to background thread perf: move provider to background thread Aug 11, 2026
@Wodann
Wodann temporarily deployed to github-action-benchmark August 11, 2026 15:35 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark August 11, 2026 15:35 — with GitHub Actions Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changeset needed This PR doesn't require a changeset

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants