Skip to content

fix(perps): coalesce candle subscribe races to cut HL 429s cp-13.28.0 - #41917

Merged
aganglada merged 20 commits into
mainfrom
feat/tat-2986-investigate-perps-429s
Apr 20, 2026
Merged

fix(perps): coalesce candle subscribe races to cut HL 429s cp-13.28.0#41917
aganglada merged 20 commits into
mainfrom
feat/tat-2986-investigate-perps-429s

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Description

Under rapid perps market switching, Hyperliquid returned 429s on candle requests and activity-page REST calls. This PR removes the client-side races that caused them.

Problem 1 — candle subscribe race. Opening a market-detail page triggers three components that each call subscribeToCandles for the same {symbol, interval} key in the same tick: PerpsLayout, PerpsTVChart, and useCandleStream. Each call fires a POST /info { type: "candleSnapshot" } to Hyperliquid. The vendor client (HyperLiquidClientService) aborts duplicates via AbortController (follow-up to MetaMask/core#28141), but the server has already accepted the request and charged weight against the 1200 wgt/min per-IP limit. On rapid navigation each market switch leaks 1–2 aborted-but-charged REST calls, which is what tips us over 429.

Problem 2 — activity-page REST burst. usePerpsTransactionHistory fires perpsGetUserHistory, perpsGetOrderFills, perpsGetOrders, and perpsGetFunding in Promise.all on mount, with no in-flight dedup or cache. Rapid tab switches multiply the burst. useUserHistory has the same shape for perpsGetUserHistory alone.

Fix — coalesce at the origin of each burst.

Layer File Behaviour
Idempotent activate + 150ms deferred teardown app/scripts/controllers/perps/perps-stream-bridge.ts A second perpsActivateCandleStream for a live key short-circuits. A perpsDeactivateCandleStream waits 150ms; a matching activate inside that window cancels the teardown instead of tearing down and rebuilding.
120ms leading-edge debounce ui/providers/perps/CandleStreamChannel.ts Back-to-back UI subscribers for the same key coalesce into one connect. lastConnectAt is cleared on full disconnect/reconnect so legitimate re-subscribes after teardown fire immediately.
In-flight dedup + 10s TTL cache ui/hooks/perps/useUserHistory.ts, ui/hooks/perps/usePerpsTransactionHistory.ts via ui/hooks/perps/coalesceBackgroundRequest.ts Activity-page mount bursts dedup to a single HL call per endpoint+params. Explicit refetch() bypasses the cache so user-initiated refresh still re-runs.
Full setData on symbol/interval change ui/components/app/perps/TradingViewChart/CandlestickChart.tsx Avoids lightweight-charts crashes from partial updates during rapid market switches.

All windows are scoped — outside them the code path is unchanged, so there is no over-coalescing of genuinely new subscriptions.

Changelog

CHANGELOG entry: Fixed a bug that caused some Hyperliquid requests to be rate-limited (429) when rapidly switching between perps markets.

Related issues

Fixes: #41926 TAT-2986

Manual testing steps

  1. Check out the branch: PORT=9015 yarn start (watch mode) or yarn build:test for a clean one-shot build.
  2. Load unpacked from dist/chrome/. Unlock a perps account.
  3. Navigate through several perps markets in quick succession (BTC → ETH → SOL → HYPE → PUMP, ~1.5s dwell each), then visit the activity page, then repeat. Watch service-worker and home-page devtools network panels — no 429 responses from api.hyperliquid.xyz should appear.
  4. Unit tests:
    yarn jest app/scripts/controllers/perps/perps-stream-bridge.test --no-coverage
    yarn jest ui/providers/perps/CandleStreamChannel.test --no-coverage
    yarn jest ui/hooks/perps/useUserHistory.test --no-coverage
    yarn jest ui/hooks/perps/usePerpsTransactionHistory.test --no-coverage

Screenshots/Recordings

Evidence below is extension-only, measured via Chrome DevTools Protocol attached to the service worker + home-page targets. A scripted 10-market rotation drives the same code path as manual rapid navigation, at human-realistic pacing (1500 ms dwell per market), with a mid-rotation activity-page visit to exercise the transaction-history fetch burst.

Probe shape (identical between runs): 2 passes × 10 markets × 1500 ms dwell + 1 activity visit per pass. Counts every api.hyperliquid.xyz response + every matching console error from sw and home.

HL requests 200 429 Error messages matched
Before (main, 4ede4f596d) 20 17 3 (15 %) 32 (WebSocketRequestError timeouts, HttpRequestError 429)
After (this branch, 42db5f07ad) 20 20 0 0

No cross-platform comparison is claimed — the mobile build was not instrumented with the same network probe.

Recipe JSON — rate-limit-10-market-stress.json (click to expand)

The scripted rotation above runs via an internal CDP recipe runner that is not yet available to external reviewers. The recipe graph itself is reproducible by hand: each run{1,2}-<symbol> node is navigate → 1500ms wait → wait for perps-market-detail-page test-id, with a mid-rotation detour through /perps/activity.

{
  "title": "Perps 10-market rapid-switch rate-limit proof (core + HIP-3)",
  "validate": {
    "workflow": {
      "pre_conditions": ["wallet.unlocked"],
      "entry": "setup-install-rl-probe",
      "teardown": [
        { "id": "teardown-stop-rl-probe", "action": "cdp_probe", "phase": "stop", "name": "hl-rate-limit" }
      ],
      "nodes": {
        "setup-install-rl-probe": {
          "action": "cdp_probe",
          "phase": "start",
          "name": "hl-rate-limit",
          "scope": "both",
          "url_pattern": "api\\.hyperliquid\\.xyz",
          "message_pattern": "WebSocketRequestError|HttpRequestError|Too Many Requests|\\b429\\b",
          "status_match": [429],
          "stream": true,
          "next": "setup-nav-perps"
        },
        "setup-nav-perps":        { "action": "call", "ref": "perps/navigate-perps-tab", "next": "run1-btc" },

        "run1-btc":         { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "BTC" },      "next": "run1-btc-dwell" },
        "run1-btc-dwell":   { "action": "wait", "ms": 1500, "next": "run1-btc-assert" },
        "run1-btc-assert":  { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-eth" },

        "run1-eth":         { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "ETH" },      "next": "run1-eth-dwell" },
        "run1-eth-dwell":   { "action": "wait", "ms": 1500, "next": "run1-eth-assert" },
        "run1-eth-assert":  { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-sol" },

        "run1-sol":         { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "SOL" },      "next": "run1-sol-dwell" },
        "run1-sol-dwell":   { "action": "wait", "ms": 1500, "next": "run1-sol-assert" },
        "run1-sol-assert":  { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-hype" },

        "run1-hype":        { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "HYPE" },     "next": "run1-hype-dwell" },
        "run1-hype-dwell":  { "action": "wait", "ms": 1500, "next": "run1-hype-assert" },
        "run1-hype-assert": { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-pump" },

        "run1-pump":        { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "PUMP" },     "next": "run1-pump-dwell" },
        "run1-pump-dwell":  { "action": "wait", "ms": 1500, "next": "run1-pump-assert" },
        "run1-pump-assert": { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-activity-nav" },

        "run1-activity-nav":    {
          "action": "eval_sync",
          "expression": "(function(){try{location.hash='#/perps/activity';return JSON.stringify({navigated:true});}catch(e){return JSON.stringify({navigated:false,error:String(e&&e.message||e)});}})()",
          "assert": { "all": [ { "operator": "eq", "field": "navigated", "value": true } ] },
          "next": "run1-activity-dwell"
        },
        "run1-activity-dwell":  { "action": "wait", "ms": 1500, "next": "run1-activity-assert" },
        "run1-activity-assert": { "action": "wait_for", "test_id": "perps-activity-page", "timeout_ms": 3000, "next": "run1-tsla" },

        "run1-tsla":        { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "xyz:TSLA" }, "next": "run1-tsla-dwell" },
        "run1-tsla-dwell":  { "action": "wait", "ms": 1500, "next": "run1-tsla-assert" },
        "run1-tsla-assert": { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-nvda" },

        "run1-nvda":        { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "xyz:NVDA" }, "next": "run1-nvda-dwell" },
        "run1-nvda-dwell":  { "action": "wait", "ms": 1500, "next": "run1-nvda-assert" },
        "run1-nvda-assert": { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-aapl" },

        "run1-aapl":        { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "xyz:AAPL" }, "next": "run1-aapl-dwell" },
        "run1-aapl-dwell":  { "action": "wait", "ms": 1500, "next": "run1-aapl-assert" },
        "run1-aapl-assert": { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-gold" },

        "run1-gold":        { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "xyz:GOLD" }, "next": "run1-gold-dwell" },
        "run1-gold-dwell":  { "action": "wait", "ms": 1500, "next": "run1-gold-assert" },
        "run1-gold-assert": { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run1-coin" },

        "run1-coin":        { "action": "call", "ref": "perps/navigate-to-market-detail", "params": { "symbol": "xyz:COIN" }, "next": "run1-coin-dwell" },
        "run1-coin-dwell":  { "action": "wait", "ms": 1500, "next": "run1-coin-assert" },
        "run1-coin-assert": { "action": "wait_for", "test_id": "perps-market-detail-page", "timeout_ms": 3000, "next": "run2-btc" },

        "// run2-*": "same shape as run1-* over [BTC, ETH, SOL, HYPE, PUMP, (activity visit), xyz:TSLA, xyz:NVDA, xyz:AAPL, xyz:GOLD, xyz:COIN]",

        "ac-no-error-boundary": {
          "action": "eval_sync",
          "expression": "(function(){var hasError=!!document.querySelector('[data-testid=\"error-boundary\"], [data-testid=\"perps-error-fallback\"]');var onDetail=!!document.querySelector('[data-testid=\"perps-market-detail-page\"]');return JSON.stringify({hasError:hasError,onDetail:onDetail});})()",
          "assert": { "all": [
            { "operator": "eq", "field": "hasError", "value": false },
            { "operator": "eq", "field": "onDetail", "value": true }
          ] },
          "next": "ac-rate-limit-probe"
        },

        "ac-rate-limit-probe": {
          "action": "cdp_probe",
          "phase": "stop",
          "name": "hl-rate-limit",
          "assert": { "all": [
            { "operator": "eq", "field": "network.matchedStatus", "value": 0 },
            { "operator": "eq", "field": "messages.total", "value": 0 }
          ] },
          "next": "done"
        },

        "done": { "action": "end", "status": "pass" }
      }
    }
  }
}

Reproducing by hand: navigate to each symbol in [BTC, ETH, SOL, HYPE, PUMP], wait ~1.5 s each, open #/perps/activity, wait ~1.5 s, then [xyz:TSLA, xyz:NVDA, xyz:AAPL, xyz:GOLD, xyz:COIN] — and repeat once. With DevTools Network open, filter to api.hyperliquid.xyz. Pre-fix you should see a handful of 429 responses; post-fix none.

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Touches perps streaming lifecycle and transaction-history fetching, where timing/teardown/coalescing bugs could lead to stale UI data or missed/unexpected subscriptions despite added test coverage.

Overview
Reduces Hyperliquid rate-limit bursts during rapid perps navigation by making candle streaming idempotent and debounce/coalesced across layers.

In PerpsStreamBridge, perpsActivateCandleStream now dedups concurrent activations, cancels pending teardowns, and defers perpsDeactivateCandleStream teardown by 150ms (with generation-based guards on destroy()). In the UI, CandleStreamChannel adds a 120ms reconnect debounce anchored to the last disconnect, and the candlestick chart forces full setData on symbol/interval changes to avoid incremental-update crashes.

Adds a reusable coalesceBackgroundRequest helper (in-flight dedup + short TTL cache + scoped clearing) and applies it to useUserHistory and usePerpsTransactionHistory, including cache invalidation on explicit refetch() and a forceFreshOnMount option used by PerpsActivityPage; PerpsStreamManager clears coalesced requests on scope resets. Tests are expanded accordingly.

Reviewed by Cursor Bugbot for commit f851e9d. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbotv2

metamaskbotv2 Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor
Builds ready [d5d15a1] [reused from 4ede4f5]
⚡ Performance Benchmarks (Total: 🟢 6 pass · 🟡 9 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24597993748 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -20%
  • loadNewAccount/total: -20%
  • bridgeUserActions/bridge_load_page: -19%
  • bridgeUserActions/bridge_load_asset_picker: -43%
  • bridgeUserActions/total: -17%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.5s
  • 🟡 confirmTx/FCP: p75 2.5s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -23%
  • startupStandardHome/load: -13%
  • startupStandardHome/domContentLoaded: -15%
  • startupStandardHome/backgroundConnect: +13%
  • startupStandardHome/firstReactRender: -10%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -19%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -15%
  • startupStandardHome/load: -10%
  • startupStandardHome/domContentLoaded: -10%
  • startupStandardHome/firstPaint: +21%
  • startupStandardHome/backgroundConnect: -30%
  • startupStandardHome/firstReactRender: -19%
  • startupStandardHome/loadScripts: -10%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -13%
  • startupStandardHome/domInteractive: -29%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/numNetworkReqs: -26%
  • startupStandardHome/domInteractive: -11%
  • startupStandardHome/initialActions: +14%
  • startupStandardHome/setupStore: -54%
  • startupStandardHome/numNetworkReqs: -24%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟡 [Show logs]
onboardingNewWallet🟡 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs] · 🟡 cls
sendTransactions🟢 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -86%
  • onboardingImportWallet/confirmSrpToPwForm: -15%
  • onboardingImportWallet/pwFormToMetricsScreen: -12%
  • onboardingImportWallet/metricsToWalletReadyScreen: -31%
  • onboardingImportWallet/doneButtonToHomeScreen: -11%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: -98%
  • onboardingImportWallet/total: -35%
  • onboardingNewWallet/srpButtonToPwForm: -79%
  • onboardingNewWallet/createPwToRecoveryScreen: -10%
  • onboardingNewWallet/skipBackupToMetricsScreen: -68%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -22%
  • onboardingNewWallet/doneButtonToAssetList: +16%
  • solanaAssetDetails/assetClickToPriceChart: -27%
  • solanaAssetDetails/total: -27%
  • importSrpHome/loginToHomeScreen: -23%
  • importSrpHome/openAccountMenuAfterLogin: -58%
  • importSrpHome/homeAfterImportWithNewWallet: +59%
  • importSrpHome/total: +50%
  • swap/openSwapPageFromHome: -97%
  • swap/fetchAndDisplaySwapQuotes: -62%
  • swap/total: -68%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 onboardingImportWallet/FCP: p75 2.1s
  • 🟡 onboardingNewWallet/FCP: p75 2.0s
  • 🟡 assetDetails/INP: p75 256ms
  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.6s
  • 🟡 importSrpHome/INP: p75 280ms
  • 🟡 importSrpHome/FCP: p75 2.6s
  • 🟡 importSrpHome/CLS: p75 0.157
  • 🟡 swap/FCP: p75 2.6s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs
  • background: 0 Bytes (0%)
  • ui: 0 Bytes (0%)
  • common: 0 Bytes (0%)

@abretonc7s

abretonc7s commented Apr 18, 2026

Copy link
Copy Markdown
Contributor Author
Run Duration Model Nudges Grade Cost
bd6e1595 20m opus 0 ungraded $2.1625
Worker report

TAT-2986 — Investigate residual 429s on Extension Perps

Ticket: TAT-2986
PR: #41917
Branch: feat/tat-2986-investigate-perps-429s

Summary

Reproduced the residual HTTP 429 rate-limit live on dev1 (perps-funded): 3 × 429 across 4 rapid-switch probe runs (~23% rate) on POST https://api.hyperliquid.xyz/info candleSnapshot. Consistent with the Slack thread ("still there but significantly better than before"). Zero production code changes — pure CDP Network.responseReceived on the service-worker target captured every HL response with status + timing. Architectural diagnosis from the code alone confirmed hypothesis 1 (UI→SW abort race across the postMessage bridge).

Changes

No production code changes. This is an investigation-only PR.

  • temp/.task/feat/tat-2986-0418-133136/probe-rapid-switch.js — Node CDP probe that attaches Network.enable to SW + home + offscreen targets, drives click-based market rotation (BTC→ETH→SOL×2), captures HL responses.
  • temp/.task/feat/tat-2986-0418-133136/artifacts/INVESTIGATION.md — full investigation log with path, dead ends, and race signature.
  • temp/.task/feat/tat-2986-0418-133136/artifacts/comparison.md — hypothesis ranking + mobile↔extension architectural table + fix sketch.
  • temp/.task/feat/tat-2986-0418-133136/artifacts/recipe.json + recipe-quality.json — 16-node validation recipe hand-off for QA.
  • temp/.task/feat/tat-2986-0418-133136/artifacts/live-capture-run-{1,2,3}.json, live-capture.json — raw CDP captures (13 HL requests, 3 × 429).

Reproduction evidence

4 rapid-switch runs, click-driven via [data-testid=explore-markets-{SYMBOL}]history.back() with 200ms between steps:

Run HL reqs 429s Signature
1 4 0 Cold bridge — positions/account calls, no candles fired
2 5 2 BTC + ETH candle 429s in 550ms window mid-switch
3 2 0 Cooled off
4 2 1 SOL candle 429 on final rotation
Total 13 3 ~23% 429 rate

The race signature (run-2)

429  BTC  start=115469 end=115662   POST /info candleSnapshot BTC 5m
    nav  switch-to-BTC at 115668    (UI lands 6ms after BTC fetch ended)
    nav  back at 115869             (UI already leaving BTC)
429  ETH  start=115891 end=116012   POST /info candleSnapshot ETH 5m
    nav  switch-to-ETH at 116089    (UI lands 77ms after ETH fetch ended)
    nav  back at 116293             (UI already leaving ETH)
200  SOL  start=116304 end=116427   (HL window reopened)

Two 429s in a 550ms window — rate-limit saturated by prior coalescing of candle subscriptions whose deactivate_candle never arrived in time to abort the in-flight fetch. Matches the H1 prediction in comparison.md.

Hypothesis ranking

# Hypothesis Verdict Evidence
1 UI→SW abort raceperpsDeactivateCandleStream is async across postMessage, so tear-down lands after the candle fetch completes Confirmed Run-2 live evidence: 2× 429 for symbols the UI was already leaving. Static: perps-stream-bridge.ts:181-207 (deactivate returns void).
2 Multi-view fan-out (popup + fullscreen) Untested Probe drove one surface; needs dual-surface variant
3 StrictMode double-invoke Unlikely Only 2-5 candle requests per 12-nav cycle, not double
4 Per-IP HL budget saturation independent of UI Secondary 429s cluster with rapid-switch, not evenly across 90s
5 Stagger/debounce constants diverged from mobile Low REST_HYDRATION_STAGGER_MS = 200 unchanged; 429s fire within one stagger window

Fix sketch

Location: app/scripts/controllers/perps/perps-stream-bridge.ts:181-207 + ui/providers/perps/CandleStreamChannel.ts:336-395

  1. Idempotent activate — compute key = #candleSubscriptionKey(symbol, interval) before awaiting #initAndActivate(). If #dynamicUnsubs[key] exists, return 'ok' immediately.
  2. Deferred deactivate — wrap #tearDownDynamicKey in a 150ms setTimeout keyed by symbol+interval. Cancel on matching activate within window. Collapses rapid A→B→A flip-flops.
  3. UI leading-edge debounce — 120ms debounce on CandleStreamChannel.connect prevents React unmount→remount from hitting the SW.

Combined effect: 6-market cycle in ~1.2s produces one candle request for the landing symbol instead of six activate/deactivate pairs. Preserves AbortController guard for genuine user-left-chart case.

Test plan

  • Probe reproduction: 4 runs executed live, 3 × 429 captured (see live-capture*.json)
  • Recipe schema + dry-run: pass (node validate-recipe.js --recipe ... --dry-run)
  • Live recipe run: blocked — perps/navigate-to-market-detail flow uses perps-balance-dropdown selector that doesn't resolve on this slot. Probe script (click-based) succeeded on the same slot, so the underlying behavior is exercised; the recipe is a hand-off for QA / live-extension runs.
  • Lint + verify-locales + circular-deps strict gate: pass (no code changes).

Environment notes

  • dev1 (0x8Dc623...69003) IS perps-funded ($28.24, 0.00584 ETH, ETH 3x long). Probe script used the default account without remapping.
  • Direct hash nav to #/perps-market-details/{SYMBOL} crashes the router — the valid SPA route is #/perps/market/{SYMBOL}, reachable only by clicking [data-testid=explore-markets-{SYMBOL}].
  • stateHooks.submitRequestToBackground hangs for all methods in this slot (env issue, baseline-confirmed pre-investigation); probe routed around it by observing network directly via CDP instead of asking the SW for ring-buffer data.

Self-Review Fixes

  • artifacts/recipe.json — rewrote to drop SW-RPC dependencies (perpsResetTat2986, perpsGetTat2986Log, perpsGetTat2986Net). The planned instrumentation RPCs from TASK.md §4.1 were intentionally not landed (this PR carries zero production code). New recipe is a nav-only driver with DOM+URL assertions; network evidence stays with probe-rapid-switch.js. Live run: 18/18 pass in 2.4s on CDP port 6665.
  • artifacts/recipe-quality.json — updated rationale to match the nav-only design; evidence_efficiency + coverage_honesty remain warn and now honestly reflect that 429 detection is out-of-band by design.
  • artifacts/pr-description.md — added explicit "No production code changes — investigation-only" callout so reviewers don't look for a diff that isn't there; replaced placeholder screenshot section with the actual videos (extension-probe.mp4, mobile-probe.mp4) + BENCHMARK-REPORT.md; updated numbers with mobile parity (77/0 vs ext 19/4).
  • TASK.md:182 (step 17) — replaced the stale "fixture lacks a perps-provisioned account" rationale with the real review-time blocker (missing SW handlers, which no longer applies after the recipe rewrite). Dev1 is perps-provisioned ($11,245 balance, active ETH position observed live).

@abretonc7s
abretonc7s marked this pull request as ready for review April 18, 2026 08:40
PerpsLayout mount, chart mount, and stream activation on market-detail
navigation race to call subscribeToCandles with the same key. Each call
fires a candleSnapshot REST. The vendor client aborts the in-flight one
but HL has already charged weight for every request sent, contributing
to the residual 429s.

Three defense layers:
- Idempotent activate at the bridge: same symbol/interval key
  short-circuits a duplicate activate.
- 150ms deferred teardown: deactivate followed by activate within 150ms
  cancels the pending teardown instead of tearing down and re-subscribing.
- 120ms leading-edge debounce in CandleStreamChannel for back-to-back
  subscribers requesting the same key.

Race-probe A/B: scenarios A (2x activate, 50ms apart) and B (deactivate +
re-activate within 150ms) drop from 2 REST requests to 1. Scenario C
(300ms gap, outside defer window) remains 2 as expected - no
over-coalescing.
@abretonc7s
abretonc7s requested a review from a team as a code owner April 18, 2026 23:51
@abretonc7s abretonc7s changed the title feat: Investigate residual rate-limit (429) issues on Extension Perps fix(perps): coalesce candle subscribe races to cut HL 429s (TAT-2986) Apr 18, 2026
@metamaskbotv2

metamaskbotv2 Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

✨ Files requiring CODEOWNER review ✨

👨‍🔧 @MetaMask/perps (13 files, +1008 -63)
  • 📁 app/
    • 📁 scripts/
      • 📁 controllers/
        • 📁 perps/
          • 📄 perps-stream-bridge.test.ts +185 -13
          • 📄 perps-stream-bridge.ts +104 -8
  • 📁 ui/
    • 📁 components/
      • 📁 app/
        • 📁 perps/
          • 📁 perps-candlestick-chart/
            • 📄 perps-candlestick-chart.tsx +29 -3
    • 📁 hooks/
      • 📁 perps/
        • 📄 coalesceBackgroundRequest.test.ts +169 -0
        • 📄 coalesceBackgroundRequest.ts +125 -0
        • 📄 usePerpsTransactionHistory.test.ts +7 -0
        • 📄 usePerpsTransactionHistory.ts +125 -17
        • 📄 useUserHistory.test.ts +6 -0
        • 📄 useUserHistory.ts +51 -8
    • 📁 pages/
      • 📁 perps/
        • 📄 perps-activity-page.tsx +10 -9
    • 📁 providers/
      • 📁 perps/
        • 📄 CandleStreamChannel.test.ts +113 -1
        • 📄 CandleStreamChannel.ts +80 -4
        • 📄 PerpsStreamManager.ts +4 -0

@metamaskbotv2

metamaskbotv2 Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor
Builds ready [e8b5125]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24616652890 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -22%
  • loadNewAccount/total: -22%
  • bridgeUserActions/bridge_load_asset_picker: -47%
  • bridgeUserActions/total: -16%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.6s
  • 🟡 confirmTx/FCP: p75 2.6s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -28%
  • startupStandardHome/load: -17%
  • startupStandardHome/domContentLoaded: -19%
  • startupStandardHome/firstReactRender: -18%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -22%
  • startupStandardHome/numNetworkReqs: -29%
  • startupStandardHome/uiStartup: -14%
  • startupStandardHome/domInteractive: +21%
  • startupStandardHome/backgroundConnect: -31%
  • startupStandardHome/firstReactRender: -19%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -13%
  • startupStandardHome/domInteractive: -48%
  • startupStandardHome/backgroundConnect: +11%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/numNetworkReqs: -34%
  • startupStandardHome/uiStartup: -13%
  • startupStandardHome/domInteractive: -45%
  • startupStandardHome/initialActions: -43%
  • startupStandardHome/setupStore: -57%
  • startupStandardHome/numNetworkReqs: -29%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -83%
  • onboardingImportWallet/metricsToWalletReadyScreen: -34%
  • onboardingImportWallet/doneButtonToHomeScreen: -75%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +27%
  • onboardingImportWallet/total: -44%
  • onboardingNewWallet/srpButtonToPwForm: -76%
  • onboardingNewWallet/skipBackupToMetricsScreen: -68%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -22%
  • onboardingNewWallet/doneButtonToAssetList: -25%
  • onboardingNewWallet/total: -26%
  • assetDetails/assetClickToPriceChart: -46%
  • assetDetails/total: -46%
  • solanaAssetDetails/assetClickToPriceChart: -72%
  • solanaAssetDetails/total: -72%
  • importSrpHome/openAccountMenuAfterLogin: -84%
  • importSrpHome/homeAfterImportWithNewWallet: -67%
  • importSrpHome/total: -59%
  • sendTransactions/openSendPageFromHome: -17%
  • sendTransactions/reviewTransactionToConfirmationPage: +35%
  • sendTransactions/total: +33%
  • swap/openSwapPageFromHome: -97%
  • swap/fetchAndDisplaySwapQuotes: +32%
  • swap/total: +12%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.8s
  • 🟡 solanaAssetDetails/FCP: p75 2.6s
  • 🟡 importSrpHome/FCP: p75 2.5s
  • 🟡 sendTransactions/FCP: p75 2.5s
  • 🟡 swap/FCP: p75 2.5s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs
  • background: 489 Bytes (0.01%)
  • ui: 422 Bytes (0%)
  • common: 20 Bytes (0%)

@abretonc7s abretonc7s changed the title fix(perps): coalesce candle subscribe races to cut HL 429s (TAT-2986) fix(perps): coalesce candle subscribe races to cut HL 429s Apr 19, 2026
Clears lastConnectAt when the channel disconnects or explicitly
reconnects so the 120ms leading-edge debounce only coalesces
genuine connect-fail-connect thrashing, not legitimate re-subscribe
after full teardown. Fixes CandleStreamChannel unit tests.
Comment thread ui/providers/perps/CandleStreamChannel.ts Outdated
@abretonc7s abretonc7s added the DO-NOT-MERGE Pull requests that should not be merged label Apr 19, 2026
@metamaskbotv2

metamaskbotv2 Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor
Builds ready [4847c2d]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24622749785 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -75%
  • loadNewAccount/total: -75%
  • bridgeUserActions/bridge_load_page: -22%
  • bridgeUserActions/bridge_load_asset_picker: -45%
  • bridgeUserActions/total: -12%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.5s
  • 🟡 confirmTx/FCP: p75 2.5s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -26%
  • startupStandardHome/load: -15%
  • startupStandardHome/domContentLoaded: -17%
  • startupStandardHome/firstReactRender: -14%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -19%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -19%
  • startupStandardHome/load: -14%
  • startupStandardHome/domContentLoaded: -14%
  • startupStandardHome/firstPaint: +12%
  • startupStandardHome/backgroundConnect: -34%
  • startupStandardHome/firstReactRender: -27%
  • startupStandardHome/loadScripts: -14%
  • startupStandardHome/setupStore: -13%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -30%
  • startupStandardHome/load: -24%
  • startupStandardHome/domContentLoaded: -24%
  • startupStandardHome/domInteractive: -65%
  • startupStandardHome/firstReactRender: -20%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -25%
  • startupStandardHome/setupStore: -25%
  • startupStandardHome/numNetworkReqs: -32%
  • startupStandardHome/uiStartup: -20%
  • startupStandardHome/load: -13%
  • startupStandardHome/domContentLoaded: -13%
  • startupStandardHome/domInteractive: -70%
  • startupStandardHome/initialActions: +14%
  • startupStandardHome/loadScripts: -13%
  • startupStandardHome/setupStore: -50%
  • startupStandardHome/numNetworkReqs: -34%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -84%
  • onboardingImportWallet/metricsToWalletReadyScreen: -28%
  • onboardingImportWallet/doneButtonToHomeScreen: -76%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +27%
  • onboardingImportWallet/total: -42%
  • onboardingNewWallet/srpButtonToPwForm: -79%
  • onboardingNewWallet/skipBackupToMetricsScreen: -68%
  • onboardingNewWallet/doneButtonToAssetList: -22%
  • onboardingNewWallet/total: -24%
  • assetDetails/assetClickToPriceChart: -45%
  • assetDetails/total: -45%
  • solanaAssetDetails/assetClickToPriceChart: -72%
  • solanaAssetDetails/total: -72%
  • importSrpHome/loginToHomeScreen: -14%
  • importSrpHome/openAccountMenuAfterLogin: -53%
  • importSrpHome/homeAfterImportWithNewWallet: -67%
  • importSrpHome/total: -60%
  • sendTransactions/openSendPageFromHome: -18%
  • sendTransactions/selectTokenToSendFormLoaded: -17%
  • sendTransactions/reviewTransactionToConfirmationPage: +38%
  • sendTransactions/total: +38%
  • swap/openSwapPageFromHome: -96%
  • swap/fetchAndDisplaySwapQuotes: +32%
  • swap/total: +11%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.5s
  • 🟡 importSrpHome/FCP: p75 2.6s
  • 🟡 sendTransactions/FCP: p75 2.5s
  • 🟡 swap/FCP: p75 2.6s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs
  • background: 489 Bytes (0.01%)
  • ui: 474 Bytes (0.01%)
  • common: 20 Bytes (0%)

Rapid market switches (e.g. xyz:AAPL → xyz:GOLD) could land on the
update effect when the new series happened to have the same candle
count as the previous market. That pushed the new symbol's candle
through the incremental `.update()` path, feeding lightweight-charts
a data point older than its last stored time, which throws
"Cannot update oldest data, last time=..., new time=..." and trips
the MetaMask error boundary.

Track the last-filled symbol + interval and force a full
`setData()` replace when either changes, reserving the live-tick
and append paths for same-series updates only. Also scroll to
real-time on identity changes so the chart opens aligned.
The perps activity page mounts fire four HL REST calls in parallel
— getUserHistory, getOrderFills, getOrders, getFunding — on every
mount. Rapid navigation into/out of the activity page duplicated
that burst and chewed into the 1200 wgt/min HL budget, contributing
to residual 429s after the candle-subscribe fix.

Add a module-level coalescing helper (10 s TTL + in-flight dedup)
and wire it around the four calls in useUserHistory and
usePerpsTransactionHistory. Explicit `refetch()` paths invalidate
the cache so pull-to-refresh still hits the network.

Also make usePerpsTransactionHistory start in loading state when
it will auto-fetch on mount so consumers render a skeleton instead
of flashing "No transactions yet" on the frame before the effect
fires.
Comment thread ui/hooks/perps/usePerpsTransactionHistory.ts
- coalesceBackgroundRequest: invalidate() now evicts cache only. Dropping
  the in-flight entry caused duplicate HL requests when a second caller
  (e.g. activity-page forceFreshOnMount) invalidated while another hook
  instance's fetch was still running. The in-flight snapshot is by
  definition the freshest server data, so concurrent callers must share
  it instead of firing a duplicate request.
- usePerpsTransactionHistory / useUserHistory: add fetchGenerationRef so
  resolutions from a pre-scope-change fetch cannot overwrite state for
  the newer (account / testnet / time-range) scope after a rapid switch.
- Switch coalesce cache keys from JSON.stringify to pipe-delimited
  strings; CaipAccountId and perpsScopeKey contain ':' but never '|',
  so fields remain unambiguous without the per-render encode cost.
- Cross-reference CANDLE_TEARDOWN_DEFER_MS (150 ms bridge) and
  CONNECT_DEBOUNCE_MS (120 ms UI) so the 30 ms margin is documented.
Comment thread ui/hooks/perps/usePerpsTransactionHistory.ts
…nt switch)

- Add clearAllCoalescedRequests() that drops both the TTL cache and any
  in-flight promises. Unlike invalidateCoalescedRequest() (same-scope
  refetch, preserves in-flight), a scope change makes any in-flight
  response definitionally wrong — the next caller must start fresh.
- Wire into PerpsStreamManager's three scope-teardown paths: address
  change in ensureInitialized(), clearAllCaches(), and reset(). This
  guarantees no previous-scope response can leak into the new scope
  after a wallet lock / sign-out / account switch / testnet toggle.
- Clarify the 10s TTL rationale: passive consumers (recent-activity
  preview) are the only readers; top-level consumers bypass via
  forceFreshOnMount.

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 09ea79a. Configure here.

Comment thread ui/providers/perps/CandleStreamChannel.ts Outdated
… reconnect

When a subscriber comes back n ms after disconnect(), connect() was
scheduling another full CONNECT_DEBOUNCE_MS wait instead of the remaining
CONNECT_DEBOUNCE_MS - n. Any remount gap above ~30 ms pushed the UI
reconnect past the bridge's 150 ms teardown defer, which committed the
disconnect and forced a fresh perpsActivateCandleStream — the exact
unsubscribe/resubscribe burst this debounce exists to coalesce.

The debounce window is anchored to lastDisconnectAt, so the timer must
fire at (lastDisconnectAt + CONNECT_DEBOUNCE_MS) regardless of when the
resubscribe arrives inside that window.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
79.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@metamaskbotv2

metamaskbotv2 Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor
Builds ready [f851e9d]
⚡ Performance Benchmarks (Total: 🟢 7 pass · 🟡 8 warn · 🔴 0 fail)

Baseline (latest main): 71bd826 | Date: 10/14/58243 | Pipeline: 24654347961 | Baseline logs

Interaction Benchmarks · Samples: 5
Benchmarkchrome-browserify
loadNewAccount🟡 [Show logs]
confirmTx🟡 [Show logs]
bridgeUserActions🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • loadNewAccount/load_new_account: -20%
  • loadNewAccount/total: -20%
  • bridgeUserActions/bridge_load_asset_picker: -42%
  • bridgeUserActions/total: -16%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 loadNewAccount/FCP: p75 2.5s
  • 🟡 confirmTx/FCP: p75 2.5s
  • 🟡 bridgeUserActions/FCP: p75 2.5s
Startup Benchmarks · Samples: 100
Benchmarkchrome-browserifychrome-webpackfirefox-browserifyfirefox-webpack
startupStandardHome🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]🟢 [Show logs]

📈 Results compared to the previous 5 runs on main

  • startupStandardHome/uiStartup: -24%
  • startupStandardHome/load: -13%
  • startupStandardHome/domContentLoaded: -15%
  • startupStandardHome/firstReactRender: -21%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/loadScripts: -18%
  • startupStandardHome/numNetworkReqs: -37%
  • startupStandardHome/uiStartup: -13%
  • startupStandardHome/firstPaint: +17%
  • startupStandardHome/backgroundConnect: -26%
  • startupStandardHome/firstReactRender: -23%
  • startupStandardHome/numNetworkReqs: -44%
  • startupStandardHome/uiStartup: -15%
  • startupStandardHome/domInteractive: -59%
  • startupStandardHome/initialActions: -33%
  • startupStandardHome/numNetworkReqs: -32%
  • startupStandardHome/uiStartup: -20%
  • startupStandardHome/load: -12%
  • startupStandardHome/domContentLoaded: -12%
  • startupStandardHome/domInteractive: -68%
  • startupStandardHome/firstReactRender: -12%
  • startupStandardHome/initialActions: -43%
  • startupStandardHome/loadScripts: -12%
  • startupStandardHome/setupStore: -60%
  • startupStandardHome/numNetworkReqs: -29%
User Journey Benchmarks · Samples: 5 · mock API
Benchmarkchrome-browserify
onboardingImportWallet🟢 [Show logs]
onboardingNewWallet🟢 [Show logs]
assetDetails🟡 [Show logs]
solanaAssetDetails🟡 [Show logs]
importSrpHome🟡 [Show logs]
sendTransactions🟡 [Show logs]
swap🟡 [Show logs]

📈 Results compared to the previous 5 runs on main

  • onboardingImportWallet/srpButtonToSrpForm: -85%
  • onboardingImportWallet/metricsToWalletReadyScreen: -28%
  • onboardingImportWallet/doneButtonToHomeScreen: -78%
  • onboardingImportWallet/openAccountMenuToAccountListLoaded: +36%
  • onboardingImportWallet/total: -42%
  • onboardingNewWallet/srpButtonToPwForm: -78%
  • onboardingNewWallet/skipBackupToMetricsScreen: -65%
  • onboardingNewWallet/agreeButtonToOnboardingSuccess: -16%
  • onboardingNewWallet/doneButtonToAssetList: -36%
  • onboardingNewWallet/total: -34%
  • assetDetails/assetClickToPriceChart: -42%
  • assetDetails/total: -42%
  • solanaAssetDetails/assetClickToPriceChart: -71%
  • solanaAssetDetails/total: -71%
  • importSrpHome/loginToHomeScreen: -20%
  • importSrpHome/openAccountMenuAfterLogin: -80%
  • importSrpHome/homeAfterImportWithNewWallet: -68%
  • importSrpHome/total: -62%
  • sendTransactions/openSendPageFromHome: -30%
  • sendTransactions/selectTokenToSendFormLoaded: -18%
  • sendTransactions/reviewTransactionToConfirmationPage: +36%
  • sendTransactions/total: +33%
  • swap/openSwapPageFromHome: -96%
  • swap/fetchAndDisplaySwapQuotes: +32%
  • swap/total: +11%

🌐 Core Web Vitals — 🟢 good · 🟡 needs improvement · 🔴 poor (web.dev thresholds)

  • 🟡 assetDetails/FCP: p75 2.5s
  • 🟡 solanaAssetDetails/FCP: p75 2.5s
  • 🟡 importSrpHome/FCP: p75 2.9s
  • 🟡 sendTransactions/FCP: p75 2.6s
  • 🟡 swap/FCP: p75 2.6s
Dapp Page Load Benchmarks · Samples: 100
Benchmarkchrome-browserify
dappPageLoad🟢 [Show logs]
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 706 Bytes (0.01%)
  • ui: 2.53 KiB (0.03%)
  • common: 126 Bytes (0%)

@abretonc7s abretonc7s removed DO-NOT-MERGE Pull requests that should not be merged agentic labels Apr 20, 2026
@aganglada aganglada changed the title fix(perps): coalesce candle subscribe races to cut HL 429s fix(perps): coalesce candle subscribe races to cut HL 429s cp-13.28.0 Apr 20, 2026

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

Works nicely 🎉

@aganglada
aganglada added this pull request to the merge queue Apr 20, 2026
Merged via the queue into main with commit 20642e2 Apr 20, 2026
216 of 220 checks passed
@aganglada
aganglada deleted the feat/tat-2986-investigate-perps-429s branch April 20, 2026 09:50
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 20, 2026
@metamaskbot metamaskbot added the release-13.29.0 Issue or pull request that will be included in release 13.29.0 label Apr 20, 2026
@MajorLift

MajorLift commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 Validation Run

Verdict: ✅ the added tests have power over the coalescing guard — Claim: in-flight candle subscriptions are shared rather than duplicated. head 20642e25bec · 2026-08-02 · falsifying-test check

Note

Trial run of the MetaMask evidence skills
feedback welcome, on the finding or on whether this format is useful to a reviewer.
Not a review verdict; nothing here blocks the PR.

The sharing happens at coalesceBackgroundRequest.ts#L54 — an in-flight lookup that returns the existing promise. Reading the tests shows their shape; only removing that line and re-running shows whether they can see it. Both arms run the same 10 tests in CI, so a mutation that broke the module would show as a dropped test count rather than passing for a falsification.

Falsification probe — falsifying

Arm Mutation Result
A — baseline none Test Suites: 1 passed, 1 total Tests: 10 passed, 10 total
B — mutant ui/hooks/perps/coalesceBackgroundRequest.ts:54 replaced Test Suites: 1 failed, 1 total Tests: 2 failed, 8 passed, 10 total

The suite fails when the mechanism is removed and passes when restored, running the same 10 tests in both arms. The test has power.

Failing under mutation:

  • ● coalesceBackgroundRequest › dedups concurrent callers with the same key into one in-flight request
  • ● coalesceBackgroundRequest › invalidate() mid-flight preserves the in-flight promise so concurrent callers coalesce into one request

Produced by falsify-probe.sh at 20642e25bec1968d8ef71f75f6afc82a6d6dd6dc · node v24.13.1 · yarn.lock 3d4485d59328134a · 0 tracked changes. Run: https://github.com/MajorLift/metamask-skills/actions/runs/30750999191 — logs and artifacts attached there.

Follows from the arm above

  • Disabling the in-flight lookup fails exactly 2 of 10 tests, and the other 8 still pass — the suite is discriminating, not collapsing.
  • The same 10 tests run in both arms, so nothing here is a module that failed to load.

Open for review: this mutates one line of one file. It establishes that the tests can see the coalescing guard, not that the guard is reachable from every path that subscribes, nor that sharing is the right behaviour under an error in the shared promise.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-13.29.0 Issue or pull request that will be included in release 13.29.0 size-XL team-perps Perps team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Perps - Often rate limit with error WebSocketRequestError: 429 Too Many Requests

5 participants