Skip to content

perf(perps): measure loading and reuse terminal trends - #34948

Open
abretonc7s wants to merge 58 commits into
mainfrom
perf/perps-loading-session-stack
Open

perf(perps): measure loading and reuse terminal trends#34948
abretonc7s wants to merge 58 commits into
mainfrom
perf/perps-loading-session-stack

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

This PR improves Perps loading and measures the result across the Homepage and market detail. It now includes the former stacked PR #35146 because both parts depend on the same loading-session identity, stream provenance, context isolation, and Sentry contract.

The runtime changes are:

  • Load the Terminal v2 market snapshot through @metamask/perps-controller 12.1.0 and reuse market.trend on Homepage cards.
  • Request candle fallback only for visible markets whose Terminal trend is missing or invalid.
  • Correlate preload, connection, WebSocket, Homepage TTC/DFD, and visible-frame records through one loading-session generation.
  • Record Lite and Pro market-detail section readiness without creating duplicate duration traces.
  • Restart detail measurement when market, mode, account, network, provider, HIP-3 configuration, or app lifecycle changes.
  • Reject late chart, stream, and order-book data from the previous generation. Synchronous order-book setup failures now resolve as errors with retry UI instead of remaining stuck as loading.

Two shared APIs change in a backward-compatible way. trace.ts accepts an optional trace ID so asynchronous measurements reach the intended span. useSectionPerformance accepts optional metadata for existing Homepage TTC/DFD spans. Existing callers keep their current behavior.

Request reduction

iOS development run Hyperliquid /info candleSnapshot
Current main 18 10
Candidate with complete Terminal trends 8 0

All five visible sparklines remained rendered. A separate incomplete-snapshot run confirmed that only markets without a usable trend use candle fallback. This proves request elimination and fallback behavior. It does not claim a statistically stable end-to-end latency improvement.

Why validation took several rounds

The request reduction was straightforward. The harder part was making each timing represent the correct account, network, market, mode, and visible frame.

Device and review passes found cases that a normal happy path missed: global market data being reset on account change, old sockets completing a new generation, fallback sparklines freezing, stale chart and order-book data crossing a symbol switch, missing stats completing too early, and synchronous order-book failures never leaving loading.

The validation now checks contrarian paths before happy paths. Approval requires device-scoped raw records, HUD-visible screenshots, exact Mobile and Harness commits, no position or order mutation, and expected failures for missing, reordered, duplicated, or mixed-identity records. The canonical Recipe v1 work is tracked in experimental-metamask-harness #203.

The production dashboards mirror the documented lifecycle:

Changelog

CHANGELOG entry: Improved Perps loading by reusing Terminal market trends while retaining candle fallback when trends are unavailable

Related issues

Refs: https://consensyssoftware.atlassian.net/browse/TAT-3662

Manual testing steps

Feature: Perps loading and readiness

  Scenario: Terminal supplies complete Homepage trends
    Given an unlocked development wallet on Hyperliquid mainnet
    And Perps performance caches are cleared
    When I open the Homepage Perps section
    Then all visible sparklines render from Terminal trends
    And no candleSnapshot request is made

  Scenario: A visible market has no usable Terminal trend
    Given one visible market has no usable trend
    When the Homepage Perps section renders
    Then only that market requests candle fallback
    And its sparkline remains visible

  Scenario: Lite and Pro detail sections resolve
    Given the wallet is unlocked on Perps testnet
    When I open ETH Lite, switch to Pro, and then open BTC
    Then each screen settles with the selected market and mode
    And each detail generation records only its applicable sections

  Scenario: context changes reject stale data
    Given BTC Pro is settled on dev1
    When I switch to dev2, background and resume, then switch network in both directions
    Then the visible market remains BTC Pro
    And no prior account, network, chart, or order-book value completes the new generation

Final Android validation

Physical Pixel 6 21261FDF6001SP, Mobile e52f8e15cf83971120c0359b50f406d0d25f8b00, Harness production code 8783fac90920153d1732769d41dd48638fdc9c65. Harness PR head f9fc523574cacaa346a91f9af1c71a0500ea513e adds only the corrected continuity-test expectation.

The serialized Recipe v1 runs passed with --heal off:

  1. Adversarial network isolation: 17/17
  2. Lite/Pro detail, symbol, account, and background resume: 34/34
  3. Public perps.performance network-switch flow: 41/41
  4. Adversarial rerun from Homepage state: 17/17

Every run records dirty: false, valid provenance with no drift, recovered: [], and mutations: []. All ten screenshots have a current-run HUD and settled content.

The account-switch detail session now records replacement data after reconnect: account at 5762 ms, price at 5771 ms, and empty positions/orders at 6168 ms. The Homepage sample records one coherent Terminal v2 sequence with TTC 3834 ms and DFD 4174 ms. These are single device samples used to validate measurement semantics, not a latency benchmark.

The Harness implementation and reproducible graph are in experimental-metamask-harness #203.

Screenshots/Recordings

Before

The UI is intentionally unchanged. Before evidence is the redacted request summary with ten candleSnapshot calls.

After

Recipe screenshots confirm the expected Homepage, Lite, and Pro states with the HUD visible. The complete-trend request summary contains zero candleSnapshot calls.
image

Pre-merge author checklist

Performance checks

  • I've tested on Android
  • I've assessed the power-user scenario
  • I've instrumented key operations with Sentry traces

Pre-merge reviewer checklist

  • I've manually tested the PR.
  • I confirm that this PR addresses the acceptance criteria and includes the necessary evidence.

Note

Medium Risk
Changes affect live Perps streams, chart/order-book lifecycle, and Sentry trace correlation across account/network switches; incorrect generation handling could mis-report readiness or briefly show stale market data, but scope is UI and instrumentation rather than order execution.

Overview
This PR tightens Perps loading measurement and market-detail correctness when users switch markets, modes, accounts, or network context. Lite and Pro detail screens now drive section-level readiness (market, price, chart, stats, insights, account, order book, positions/orders) through usePerpsMarketDetailSession, reset CUF traces on generation changes, and gate PerpsMarketDetailLive on current-symbol readiness instead of a single coarse hydration check.

Market context isolation is the main runtime change: charts, candles, order books, focused price, and fullscreen/OHLC UI wait on usePerpsMarketContext and a resetKey, drop stale cross-symbol data, and report onResolved from Advanced Chart and Pro sub-panels. The market-details router forwards generationTrigger (initial, market_switch, mode_switch) from navigation params; the header market picker sets detailGenerationTrigger: 'market_switch'. useMarketInsights ignores out-of-order fetches after symbol changes.

Observability: mobile infrastructure correlates preload traces with the active loading session, routes child measurements to the correct trace id, and records controller construction time. A yarn patch on @metamask/perps-controller adds safer MYX registration, bounded HyperLiquid allMids debug logging, and guarded candle WebSocket unsubscribe. .watchmanconfig ignores temp.

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

@abretonc7s abretonc7s self-assigned this Aug 18, 2026
@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.

@metamask-ci metamask-ci Bot added the team-perps Perps team label Aug 18, 2026
@github-actions github-actions Bot added size-XL and removed size-L labels Aug 18, 2026
@abretonc7s
abretonc7s changed the base branch from perf/perps-sentry-homepage-stack to perf/perps-bootstrap-mobile August 18, 2026 23:12
@abretonc7s abretonc7s changed the title perf(perps): add loading-session milestones perf(perps): instrument app-to-live loading funnel Aug 18, 2026
@abretonc7s
abretonc7s marked this pull request as ready for review August 18, 2026 23:32
@abretonc7s
abretonc7s requested review from a team as code owners August 18, 2026 23:32
Comment thread app/components/UI/Perps/providers/PerpsAlwaysOnProvider.tsx Outdated
Comment thread app/components/Views/Homepage/Sections/Perpetuals/PerpsSectionMain.tsx Outdated
Base automatically changed from perf/perps-bootstrap-mobile to main August 19, 2026 09:55
@abretonc7s
abretonc7s force-pushed the perf/perps-loading-session-stack branch from 93654e6 to 64597a0 Compare August 19, 2026 11:40
@socket-security

socket-security Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​@​metamask/​perps-controller@​12.0.0 ⏵ 12.1.09910083 +199 +1100

View full report

@github-actions github-actions Bot added the risk:high AI analysis: high risk label Aug 19, 2026
Comment thread app/components/UI/Perps/adapters/mobileInfrastructure.ts
Comment thread app/components/UI/Perps/providers/PerpsAlwaysOnProvider.tsx Outdated
Comment thread app/components/UI/Perps/providers/PerpsStreamManager.tsx Outdated
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🧪 Flaky unit test detection

Run history flaky detection

View recent run history

Historical failure rate is a hint, not proof — review each suggestion in context. See the flaky-test-detection skill for the full pattern reference and manual audit workflow.

Failures / runs sampled per window:

File 7d 15d 30d
app/components/UI/MarketInsights/hooks/useMarketInsights.test.ts 0/135 0/235 0/381
app/components/UI/Perps/adapters/mobileInfrastructure.test.ts 0/135 0/235 0/381
app/components/UI/Perps/components/PerpsOrderHeader/PerpsOrderHeader.performance.test.tsx 0/135 0/235 0/381
app/components/UI/Perps/hooks/usePerpsMarketContext.test.ts 0/135 0/235 0/381
app/components/UI/Perps/hooks/usePerpsMarketDetailSession.test.ts 0/135 0/235 0/381
app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts 0/135 0/235 0/381
app/components/UI/Perps/hooks/usePerpsMeasurement.test.ts 0/135 0/235 0/381
app/components/UI/Perps/providers/PerpsStreamManager.test.tsx 0/135 0/235 0/381
app/components/UI/Perps/providers/channels/CandleStreamChannel.test.ts 0/135 0/235 0/381
app/components/UI/Perps/services/PerpsConnectionManager.test.ts 0/135 0/235 0/381
app/components/UI/Perps/utils/perpsLoadingSession.test.ts 0/135 0/235 0/381
app/components/Views/TrendingView/feeds/perps/usePerpsFeed.test.ts 0/135 0/235 0/381
app/util/trace.test.ts 0/135 0/235 0/381

AI-detected flaky patterns

app/components/UI/MarketInsights/hooks/useMarketInsights.test.ts

  • J4 — waitFor without assertion inside, or with an async callback (high)
    • The waitFor callback is a bare expression rather than a block with an explicit assertion. In practice this can be harder to diagnose and is more likely to hide accidental async behavior inside the callback. Using a block with a synchronous assertion is the stable pattern for polling React state.
    • Suggested fix in app/components/UI/MarketInsights/hooks/useMarketInsights.test.ts:
      -    await waitFor(() => expect(result.current.isLoading).toBe(false));
      +    await waitFor(() => {
      +      expect(result.current.isLoading).toBe(false);
      +    });
  • J4 — waitFor without assertion inside, or with an async callback (high)
    • This is the same waitFor style issue repeated in the file. Wrapping the assertion in a block makes the polling intent explicit and avoids accidental async callbacks or no-op bodies.
    • Suggested fix in app/components/UI/MarketInsights/hooks/useMarketInsights.test.ts:
      -    await waitFor(() => expect(result.current.isLoading).toBe(false));
      +    await waitFor(() => {
      +      expect(result.current.isLoading).toBe(false);
      +    });
  • J4 — waitFor without assertion inside, or with an async callback (high)
    • This waitFor call has the same pattern as the others in the file. Converting it to a block with a direct assertion keeps the test deterministic and easier to debug when the hook takes longer than expected to settle.
    • Suggested fix in app/components/UI/MarketInsights/hooks/useMarketInsights.test.ts:
      -    await waitFor(() => expect(result.current.isLoading).toBe(false));
      +    await waitFor(() => {
      +      expect(result.current.isLoading).toBe(false);
      +    });
  • J4 — waitFor without assertion inside, or with an async callback (high)
    • Repeated waitFor shorthand in this file is a small but real flake risk because it obscures the assertion boundary. Prefer the block form everywhere for consistency.
    • Suggested fix in app/components/UI/MarketInsights/hooks/useMarketInsights.test.ts:
      -    await waitFor(() => expect(result.current.isLoading).toBe(false));
      +    await waitFor(() => {
      +      expect(result.current.isLoading).toBe(false);
      +    });
  • J4 — waitFor without assertion inside, or with an async callback (high)
    • This final occurrence is the same issue: the callback should be a block with a synchronous assertion. That avoids accidental no-op callbacks and makes the polling semantics obvious.
    • Suggested fix in app/components/UI/MarketInsights/hooks/useMarketInsights.test.ts:
      -    await waitFor(() => expect(result.current.isLoading).toBe(false));
      +    await waitFor(() => {
      +      expect(result.current.isLoading).toBe(false);
      +    });

app/components/UI/Perps/adapters/mobileInfrastructure.test.ts

  • J9 — Module-level mutable let bindings not reset in beforeEach (high)
    • mockSelectVipProgramEnabled is a module-level jest.fn() whose return value is mutated per-test (e.g. mockSelectVipProgramEnabled.mockReturnValue(false) in the 'returns 0 discount' test). jest.clearAllMocks() (called in the outer beforeEach) clears call counts but does NOT reset mockReturnValue implementations. So after the 'returns 0 discount when vipProgramEnabled is false' test runs, the mock is left returning false for all subsequent tests — including any tests in the later describe blocks (createMobileClientConfig, getTerminalApiUrl, createMobileInfrastructure - terminalApi) that also call createMobileInfrastructure() and indirectly invoke the selector. This causes order-dependent failures when Jest randomizes test order.
    • Suggested fix in app/components/UI/Perps/adapters/mobileInfrastructure.test.ts:119:
      -const mockSelectVipProgramEnabled = jest.fn().mockReturnValue(true);
      -jest.mock('../../../../selectors/featureFlagController/vipProgram', () => ({
      -  selectVipProgramEnabled: (...args: unknown[]) =>
      -    mockSelectVipProgramEnabled(...args),
      -}));
      +const mockSelectVipProgramEnabled = jest.fn().mockReturnValue(true);
      +jest.mock('../../../../selectors/featureFlagController/vipProgram', () => ({
      +  selectVipProgramEnabled: (...args: unknown[]) =>
      +    mockSelectVipProgramEnabled(...args),
      +}));
      +
      +// Inside the top-level describe('createMobileInfrastructure') block:
      +beforeEach(() => {
      +  jest.clearAllMocks();
      +  mockSelectVipProgramEnabled.mockReturnValue(true); // reset to default before every test
      +});
  • J3 — Missing jest.clearAllMocks() / jest.resetAllMocks() (high)
    • The top-level describe('createMobileInfrastructure') block has a beforeEach with jest.clearAllMocks(), but the three sibling describe blocks — createMobileClientConfig (line ~484), getTerminalApiUrl (line ~537), and createMobileInfrastructure - terminalApi (line ~617) — have no beforeEach that clears mocks. All four describe blocks share the same module-level mocks (analytics.trackEvent, AnalyticsEventBuilder.createEventBuilder, recordPerpsControllerConstructedAt, Engine.context.RewardsController.getPerpsDiscountForAccount, etc.). Call counts and mock implementations set in the first describe block's tests can bleed into the later blocks when Jest runs tests in a different order (e.g. with --randomize). This is especially risky for the terminalApi describe block, which calls createMobileInfrastructure() and exercises the same mocked modules.
    • Suggested fix in app/components/UI/Perps/adapters/mobileInfrastructure.test.ts:484:
      -describe('createMobileClientConfig', () => {
      -  it('returns default config with empty strings and arrays when no env vars are set', () => {
      +describe('createMobileClientConfig', () => {
      +  beforeEach(() => {
      +    jest.clearAllMocks();
      +  });
      +
      +  it('returns default config with empty strings and arrays when no env vars are set', () => {

app/components/UI/Perps/components/PerpsOrderHeader/PerpsOrderHeader.performance.test.tsx

  • J7 — Non-deterministic data: Date.now() in mock payload (medium)
    • buildPriceUpdate embeds a live Date.now() call in the timestamp field of every mock PriceUpdate. If the production component or hook under test ever reads timestamp to make a decision (e.g. deduplicating ticks, ordering updates, or computing elapsed time), the assertion result will vary with wall-clock time and CI load. Even if the current component ignores timestamp, the pattern is fragile: a future refactor that starts using timestamp will silently introduce flakiness without any test change. Pinning the timestamp to a fixed value makes the test hermetic.
    • Suggested fix in app/components/UI/Perps/components/PerpsOrderHeader/PerpsOrderHeader.performance.test.tsx:91:
      -const buildPriceUpdate = (price: string): PriceUpdate => ({
      -  symbol: 'BTC',
      -  price,
      -  markPrice: price,
      -  percentChange24h: '4.20',
      -  timestamp: Date.now(),
      -  isTradable: true,
      -});
      +const buildPriceUpdate = (price: string): PriceUpdate => ({
      +  symbol: 'BTC',
      +  price,
      +  markPrice: price,
      +  percentChange24h: '4.20',
      +  timestamp: 1_700_000_000_000,
      +  isTradable: true,
      +});

app/components/UI/Perps/hooks/usePerpsMarketContext.test.ts

  • J9 — Module-level mutable let bindings not reset in beforeEach (high)
    • These module-level listener bindings are mutated by the mocked subscription APIs and are only partially reset in beforeEach. That is a classic order-dependence risk: if a test adds listeners or leaves a callback behind, later tests can see stale subscriptions. Resetting all listener holders in beforeEach is the right isolation boundary.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMarketContext.test.ts:26:
      -let mockContextListeners: (() => void)[] = [];
      -let mockUserContextListener: (() => void) | undefined;
      -let mockGenerationListener: (() => void) | undefined;
      +let mockContextListeners: (() => void)[] = [];
      +let mockUserContextListener: (() => void) | undefined;
      +let mockGenerationListener: (() => void) | undefined;
  • J3 — Missing jest.clearAllMocks()/resetAllMocks() between tests (high)
    • The suite resets the listener variables in beforeEach, but the mocked subscription functions themselves are shared across tests. If future tests add assertions on call counts or implementations, the lack of a consistent mock reset can cause bleed-through. Keeping the explicit jest.clearAllMocks() pattern alongside the listener resets avoids that class of flake.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMarketContext.test.ts:26:
      -let mockContextListeners: (() => void)[] = [];
      -let mockUserContextListener: (() => void) | undefined;
      -let mockGenerationListener: (() => void) | undefined;
      +let mockContextListeners: (() => void)[] = [];
      +let mockUserContextListener: (() => void) | undefined;
      +let mockGenerationListener: (() => void) | undefined;

app/components/UI/Perps/hooks/usePerpsMarketDetailSession.test.ts

  • J9 — Module-level mutable let bindings not reset in beforeEach (medium)
    • appStateListener is a describe-scoped mutable let binding with no initializer and no explicit reset in beforeEach. It is populated only as a side-effect of the hook calling AppState.addEventListener, which is captured by the spy installed in beforeEach. If the hook ever conditionally skips calling addEventListener (e.g., when a guard condition is false), appStateListener remains undefined (or holds the stale value from the previous test) when a test body calls appStateListener('background') or appStateListener('active') directly. Under --randomize or parallel execution this becomes order-dependent: a test that calls appStateListener before the spy has had a chance to capture a fresh listener will either throw 'appStateListener is not a function' or silently invoke the wrong test's listener. The fix is to initialize appStateListener to a no-op in beforeEach so that any test that calls it before the hook has registered a real listener gets a safe, predictable default rather than undefined or a stale closure.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMarketDetailSession.test.ts:100:
      -  let appState: AppStateStatus;
      -  let appStateListener: (state: AppStateStatus) => void;
      +  let appState: AppStateStatus;
      +  let appStateListener: (state: AppStateStatus) => void = () => undefined;
      +
      +  beforeEach(() => {
      +    jest.clearAllMocks();
      +    jest.useFakeTimers();
      +    mockNow = 100;
      +    mockUuidCounter = 0;
      +    mockAddress = '0xabc';
      +    mockNetwork = 'testnet';
      +    mockProvider = 'hyperliquid';
      +    mockHip3ConfigVersion = 1;
      +    mockIsMarketContextReady = true;
      +    mockIsUserContextReady = true;
      +    mockConnectionGeneration = 0;
      +    Object.keys(mockDeliveryRevisions).forEach((channel) => {
      +      mockDeliveryRevisions[channel as keyof typeof mockDeliveryRevisions] = 0;
      +    });
      +    appState = 'active';
      +    appStateListener = () => undefined; // reset to safe no-op before each test
      +    Object.defineProperty(AppState, 'currentState', {
      +      configurable: true,
      +      get: () => appState,
      +    });
      +    jest
      +      .spyOn(AppState, 'addEventListener')
      +      .mockImplementation((_, listener) => {
      +        appStateListener = listener;
      +        return { remove: jest.fn() };
      +      });
      +  });
  • J9 — Module-level mutable let bindings not reset in beforeEach (high)
    • mockNow and mockUuidCounter are module-level mutable let bindings whose values are consumed via pre-increment (++mockNow, ++mockUuidCounter) inside jest.mock factory closures. Both are correctly reset in beforeEach (mockNow = 100, mockUuidCounter = 0). However, jest.clearAllMocks() is called first in beforeEach — and several individual tests also call jest.clearAllMocks() mid-test (e.g. lines 196, 237, 271, 302) to reset spy call counts between the 'before' and 'after' phases of a single test. jest.clearAllMocks() does NOT reset these let bindings, so the counters continue incrementing across the mid-test clearAllMocks() calls. This is intentional and safe as written. The real risk is that the pre-increment side-effect means the exact numeric value returned by now() or v4() depends on how many times those functions were called earlier in the same test — including calls made by the hook during renderSession() before jest.clearAllMocks() is called mid-test. Any assertion that relies on a specific session-ID string (e.g. 'session-1') will be sensitive to the number of uuid calls made before that point in the test, making the test fragile if the hook's internal call count changes. No such exact-value assertions exist today, but the pattern is worth noting as a latent risk.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMarketDetailSession.test.ts:82:
      -let mockNow = 100;
      -jest.mock('react-native-performance', () => ({
      -  now: () => ++mockNow,
      -}));
      -
      -let mockUuidCounter = 0;
      -jest.mock('uuid', () => ({
      -  v4: () => `session-${++mockUuidCounter}`,
      -}));
      +// No immediate code change required — the current resets in beforeEach are correct.
      +// To make the counters fully robust against future exact-value assertions,
      +// consider wrapping them in jest.fn() so clearAllMocks() also resets call counts:
      +
      +const mockNowFn = jest.fn().mockImplementation(() => {
      +  mockNowValue += 1;
      +  return mockNowValue;
      +});
      +let mockNowValue = 100;
      +
      +jest.mock('react-native-performance', () => ({
      +  now: () => mockNowFn(),
      +}));
      +
      +let mockUuidValue = 0;
      +jest.mock('uuid', () => ({
      +  v4: () => `session-${++mockUuidValue}`,
      +}));
      +
      +// In beforeEach:
      +beforeEach(() => {
      +  jest.clearAllMocks();
      +  mockNowValue = 100;
      +  mockUuidValue = 0;
      +  // ... rest of beforeEach unchanged
      +});

app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts

  • J7 — Non-deterministic data: Date.now(), Math.random(), unstubbed network (medium)
    • This fixture captures the live clock at module load time. Because the file also switches to fake timers in beforeEach, the timestamp can vary across runs and across test ordering, which makes assertions that depend on this object harder to reproduce deterministically. Pinning the timestamp to a fixed constant avoids time-based drift.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts:79:
      -  const mockPriceData = {
      -    BTC: {
      -      symbol: 'BTC',
      -      price: '45000.00',
      -      timestamp: Date.now(),
      -      percentChange24h: '2.50',
      -      funding: 0.01,
      -      // openInterest is now in USD (already converted from token units * price)
      -      // For example: 22,000 BTC * $45,000 = $990M
      -      openInterest: 990000000,
      -      volume24h: 1234567890,
      -    },
      -  };
      +  const mockPriceData = {
      +    BTC: {
      +      symbol: 'BTC',
      +      price: '45000.00',
      +      timestamp: 1_700_000_000_000,
      +      percentChange24h: '2.50',
      +      funding: 0.01,
      +      // openInterest is now in USD (already converted from token units * price)
      +      // For example: 22,000 BTC * $45,000 = $990M
      +      openInterest: 990000000,
      +      volume24h: 1234567890,
      +    },
      +  };
  • J8 — jest.useFakeTimers() combined with waitFor() (high)
    • This file enables fake timers globally in beforeEach. Any later use of waitFor in the same test file would be vulnerable to the fake-timer polling conflict, so this module-level clock setup is a risk marker worth addressing alongside the deterministic timestamp fix. If waitFor is added or already exists later in the file, it should be paired with real timers or explicit timer advancement.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMarketStats.test.ts:79:
      -  const mockPriceData = {
      -    BTC: {
      -      symbol: 'BTC',
      -      price: '45000.00',
      -      timestamp: Date.now(),
      -      percentChange24h: '2.50',
      -      funding: 0.01,
      -      // openInterest is now in USD (already converted from token units * price)
      -      // For example: 22,000 BTC * $45,000 = $990M
      -      openInterest: 990000000,
      -      volume24h: 1234567890,
      -    },
      -  };
      +  const mockPriceData = {
      +    BTC: {
      +      symbol: 'BTC',
      +      price: '45000.00',
      +      timestamp: 1_700_000_000_000,
      +      percentChange24h: '2.50',
      +      funding: 0.01,
      +      // openInterest is now in USD (already converted from token units * price)
      +      // For example: 22,000 BTC * $45,000 = $990M
      +      openInterest: 990000000,
      +      volume24h: 1234567890,
      +    },
      +  };

app/components/UI/Perps/hooks/usePerpsMeasurement.test.ts

  • J10 — jest.spyOn without restoreAllMocks() (medium)
    • This spy is created in beforeEach and relies on afterEach restoration. That is usually safe, but the file also mutates AppState.currentState directly and uses shared listeners across many tests. If a test exits early, the spy can leak into the next case. Restoring the spy explicitly in afterEach keeps the suite isolated.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMeasurement.test.ts:57:
      -    jest
      -      .spyOn(AppState, 'addEventListener')
      -      .mockImplementation((_, listener) => {
      -        appStateListener = listener;
      -        return { remove: jest.fn() };
      -      });
      +    const addEventListenerSpy = jest
      +      .spyOn(AppState, 'addEventListener')
      +      .mockImplementation((_, listener) => {
      +        appStateListener = listener;
      +        return { remove: jest.fn() };
      +      });
      +    // store addEventListenerSpy in outer scope if needed
      +
  • J3 — Missing jest.clearAllMocks()/resetAllMocks() between tests (high)
    • The suite installs a fresh AppState listener spy in beforeEach, but the listener variable is shared across tests and only restoreAllMocks is called afterward. If a test fails before cleanup or if a future test adds more spies, call history and implementations can bleed between cases. Clearing and restoring mocks in a consistent teardown makes the lifecycle deterministic.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMeasurement.test.ts:57:
      -    jest
      -      .spyOn(AppState, 'addEventListener')
      -      .mockImplementation((_, listener) => {
      -        appStateListener = listener;
      -        return { remove: jest.fn() };
      -      });
      +    jest
      +      .spyOn(AppState, 'addEventListener')
      +      .mockImplementation((_, listener) => {
      +        appStateListener = listener;
      +        return { remove: jest.fn() };
      +      });
  • J3 — Missing jest.clearAllMocks()/resetAllMocks() between tests (high)
    • The suite does call jest.clearAllMocks() in beforeEach, but the AppState listener spy is recreated every test and the listener variable is shared. If a test fails before afterEach runs, later tests can inherit stale listener state. A more robust pattern is to reset the listener variable in afterEach and keep the spy lifecycle tightly scoped.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMeasurement.test.ts:57:
      -    jest
      -      .spyOn(AppState, 'addEventListener')
      -      .mockImplementation((_, listener) => {
      -        appStateListener = listener;
      -        return { remove: jest.fn() };
      -      });
      +    jest
      +      .spyOn(AppState, 'addEventListener')
      +      .mockImplementation((_, listener) => {
      +        appStateListener = listener;
      +        return { remove: jest.fn() };
      +      });
  • J10 — jest.spyOn without restoreAllMocks() (medium)
    • This is the same AppState spy pattern as the other lifecycle tests. It is restored in afterEach, but because the listener reference is module-scoped, explicit cleanup is still important to prevent cross-test leakage if the suite is interrupted or extended. Keeping the spy restoration visible in the test body or teardown makes the isolation intent clearer.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMeasurement.test.ts:57:
      -    jest
      -      .spyOn(AppState, 'addEventListener')
      -      .mockImplementation((_, listener) => {
      -        appStateListener = listener;
      -        return { remove: jest.fn() };
      -      });
      +    jest
      +      .spyOn(AppState, 'addEventListener')
      +      .mockImplementation((_, listener) => {
      +        appStateListener = listener;
      +        return { remove: jest.fn() };
      +      });
  • J3 — Missing jest.clearAllMocks()/resetAllMocks() between tests (high)
    • The shared AppState listener variable is reset in beforeEach, but the spy itself is recreated every test. That pattern is usually fine, yet it becomes flaky if a test fails before cleanup or if more shared mocks are added later. Resetting both the listener variable and the spy state in teardown is safer.
    • Suggested fix in app/components/UI/Perps/hooks/usePerpsMeasurement.test.ts:57:
      -    jest
      -      .spyOn(AppState, 'addEventListener')
      -      .mockImplementation((_, listener) => {
      -        appStateListener = listener;
      -        return { remove: jest.fn() };
      -      });
      +    jest
      +      .spyOn(AppState, 'addEventListener')
      +      .mockImplementation((_, listener) => {
      +        appStateListener = listener;
      +        return { remove: jest.fn() };
      +      });

app/components/UI/Perps/providers/PerpsStreamManager.test.tsx

  • J8 — jest.useFakeTimers() combined with waitFor (high)
    • The outer beforeEach for the entire describe('PerpsStreamManager') block calls jest.useFakeTimers(). Several nested describe blocks — including OICapStreamChannel, StreamChannel pause/resume, and StreamChannel pause reference counting — use await waitFor(...) without switching to real timers first. waitFor from @testing-library/react-native polls using real setTimeout internally; when fake timers are active, that internal polling never fires, causing the test to hang or time out intermittently. Only the MarketDataChannel describe block correctly overrides to jest.useRealTimers() in its own beforeEach. The other nested describes that use waitFor inherit fake timers from the outer scope and are therefore at risk.
    • Suggested fix in app/components/UI/Perps/providers/PerpsStreamManager.test.tsx:1:
      -beforeEach(() => {
      -    jest.clearAllMocks();
      -    jest.clearAllTimers();
      -    jest.useFakeTimers();
      -    jest.setSystemTime(new Date('2024-01-01T12:00:00.000Z'));
      -    // ...
      -  afterEach(() => {
      -    jest.clearAllTimers();
      -    jest.useRealTimers();
      -    jest.clearAllMocks();
      -    jest.restoreAllMocks();
      -    // ...
      +// Option A: add jest.useRealTimers() / jest.useFakeTimers() to each affected nested describe
      +describe('OICapStreamChannel', () => {
      +  beforeEach(() => {
      +    jest.useRealTimers(); // waitFor needs real timers
      +  });
      +  afterEach(() => {
      +    jest.useFakeTimers(); // restore for outer suite
      +  });
      +  // ... existing tests unchanged
      +});
      +
      +describe('StreamChannel pause/resume', () => {
      +  beforeEach(() => {
      +    jest.useRealTimers();
      +  });
      +  afterEach(() => {
      +    jest.useFakeTimers();
      +  });
      +  // ... existing tests unchanged
      +});
      +
      +describe('StreamChannel pause reference counting', () => {
      +  beforeEach(() => {
      +    jest.useRealTimers();
      +  });
      +  afterEach(() => {
      +    jest.useFakeTimers();
      +  });
      +  // ... existing tests unchanged
      +});
      +
      +// Option B (per-test): replace waitFor with act() + jest.runAllTimersAsync()
      +// inside each affected test that uses waitFor under fake timers.
  • J10 — jest.spyOn (or direct mock assignment) without restoreAllMocks() (medium)
    • The test manually replaces console.error with a jest mock (console.error = jest.fn()) but the preview of the test body does not show a corresponding console.error = originalError restore call. If the restore is missing (or if the test throws before reaching it), the silenced console.error leaks into every subsequent test in the file, hiding real React warnings and making other tests harder to debug or causing them to behave differently. The outer afterEach calls jest.restoreAllMocks(), but that only restores spies created with jest.spyOn — it does NOT restore direct property assignments. The correct pattern is to use jest.spyOn(console, 'error').mockImplementation(() => {}) so that restoreAllMocks() in afterEach handles cleanup automatically.
    • Suggested fix in app/components/UI/Perps/providers/PerpsStreamManager.test.tsx:1:
      -it('throws error when usePerpsStream is used outside provider', () => {
      -    const originalError = console.error;
      -    console.error = jest.fn();
      -    const TestComponentOutsideProvider = () => {
      -      usePerpsStream();
      -      return <Text>Test</Text>;
      -    };
      -    expect(() => {
      -    // ...
      +it('throws error when usePerpsStream is used outside provider', () => {
      +  jest.spyOn(console, 'error').mockImplementation(() => {});
      +  const TestComponentOutsideProvider = () => {
      +    usePerpsStream();
      +    return <Text>Test</Text>;
      +  };
      +  expect(() => {
      +    // ... rest of test body unchanged
      +  });
      +  // No manual restore needed — afterEach's jest.restoreAllMocks() handles it
      +});

app/components/UI/Perps/providers/channels/CandleStreamChannel.test.ts

  • J2 — Real timers where fake timers are needed (high)
    • This test suite is heavily timer-driven, and this case exercises deferred connection behavior. When the test relies on real timers for debounce/teardown paths, it can become flaky under load because the callback may fire later than expected or not before the assertion runs. Use fake timers and advance them deterministically for the deferred connect path.
    • Suggested fix in app/components/UI/Perps/providers/channels/CandleStreamChannel.test.ts:
      -      mockGetIsInitialized.mockReturnValue(false);
      -      mockSubscribeToCandles.mockReturnValue(jest.fn());
      -      channel.subscribe({
      -        symbol: 'BTC',
      -        interval: CandlePeriod.OneHour,
      +      mockGetIsInitialized.mockReturnValue(false);
      +      mockSubscribeToCandles.mockReturnValue(jest.fn());
      +      channel.subscribe({
      +        symbol: 'BTC',
      +        interval: CandlePeriod.OneHour,
      +        duration: TimeDuration.OneDay,
      +        callback: jest.fn(),
      +      });
      +      act(() => {
      +        jest.advanceTimersByTime(500);
      +      });
  • J2 — Real timers where fake timers are needed (high)
    • This is the same deferred-connect pattern in another test. Real-time waiting around timer-based logic is a common source of CI flakes; advancing the fake clock makes the test independent of machine speed.
    • Suggested fix in app/components/UI/Perps/providers/channels/CandleStreamChannel.test.ts:
      -      mockGetIsInitialized.mockReturnValue(false);
      -      mockSubscribeToCandles.mockReturnValue(jest.fn());
      -      channel.subscribe({
      -        symbol: 'BTC',
      -        interval: CandlePeriod.OneHour,
      +      mockGetIsInitialized.mockReturnValue(false);
      +      mockSubscribeToCandles.mockReturnValue(jest.fn());
      +      channel.subscribe({
      +        symbol: 'BTC',
      +        interval: CandlePeriod.OneHour,
      +        duration: TimeDuration.OneDay,
      +        callback: jest.fn(),
      +      });
      +      act(() => {
      +        jest.advanceTimersByTime(500);
      +      });
  • J6 — Arbitrary setTimeout/sleep in test body (high)
    • Several tests in this file rely on implicit timer passage or cleanup delays. Any real sleep or timer barrier in these flows is flaky because it depends on scheduler timing rather than the actual condition being asserted. Prefer advancing fake timers or asserting on the observable state change directly.
    • Suggested fix in app/components/UI/Perps/providers/channels/CandleStreamChannel.test.ts:
      -      const mockUnsubscribe = jest.fn();
      -      mockSubscribeToCandles.mockReturnValue(mockUnsubscribe);
      -      channel.subscribe({
      -        symbol: 'BTC',
      -        interval: CandlePeriod.OneHour,
      +      const mockUnsubscribe = jest.fn();
      +      mockSubscribeToCandles.mockReturnValue(mockUnsubscribe);
      +      channel.subscribe({
      +        symbol: 'BTC',
      +        interval: CandlePeriod.OneHour,
      +        duration: TimeDuration.OneDay,
      +        callback: jest.fn(),
      +      });
      +      act(() => {
      +        jest.runOnlyPendingTimers();
      +      });
  • J10 — jest.spyOn without restoreAllMocks() (medium)
    • This test grabs a mocked Logger module and mutates its behavior through shared mock state. In a file with many timer-heavy tests, leaving that state to suite-level cleanup increases the chance of cross-test contamination if a failure short-circuits the normal teardown path. Restoring the mock explicitly keeps the test isolated.
    • Suggested fix in app/components/UI/Perps/providers/channels/CandleStreamChannel.test.ts:
      -      const Logger = jest.requireMock('../../../../../util/Logger').default;
      -      let capturedCallback: ((data: CandleData) => void) | undefined;
      -      mockSubscribeToCandles.mockImplementation(({ callback }) => {
      -        capturedCallback = callback;
      -        return jest.fn();
      +      const Logger = jest.requireMock('../../../../../util/Logger').default;
      +      let capturedCallback: ((data: CandleData) => void) | undefined;
      +      mockSubscribeToCandles.mockImplementation(({ callback }) => {
      +        capturedCallback = callback;
      +        return jest.fn();
      +      });
      +      try {
      +        // assertions
      +      } finally {
      +        jest.restoreAllMocks();
      +      }

app/components/UI/Perps/services/PerpsConnectionManager.test.ts

  • J6 — Arbitrary setTimeout/sleep in test body (high)
    • This test uses a real wall-clock delay to simulate an in-flight connection. That makes the outcome sensitive to CI load and timer scheduling, especially in a suite that also uses fake timers elsewhere. Prefer a deferred promise that the test resolves explicitly, or advance fake timers if the production code is timer-driven.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:247:
      -      mockPerpsController.init.mockImplementation(
      -        () => new Promise((resolve) => setTimeout(resolve, 50)),
      -      );
      +      let resolveInit!: () => void;
      +      mockPerpsController.init.mockImplementation(
      +        () =>
      +          new Promise<void>((resolve) => {
      +            resolveInit = resolve;
      +          }),
      +      );
      +      // ... later in the test, after asserting the in-progress state:
      +      resolveInit();
  • J2 — Real timers where fake timers are needed (high)
    • The test is modeling asynchronous connection progress with a real timeout instead of controlling time deterministically. In a suite that already uses fake timers for debounce and grace-period behavior, this can produce intermittent hangs or slow CI runs. Use fake timers plus explicit advancement, or a manually resolved promise, so the test does not depend on elapsed wall time.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:247:
      -      mockPerpsController.init.mockImplementation(
      -        () => new Promise((resolve) => setTimeout(resolve, 50)),
      -      );
      +      let resolveInit!: () => void;
      +      mockPerpsController.init.mockImplementation(
      +        () =>
      +          new Promise<void>((resolve) => {
      +            resolveInit = resolve;
      +          }),
      +      );
      +      // ... later in the test, after asserting the in-progress state:
      +      resolveInit();
  • J2 — Real timers where fake timers are needed (high)
    • This test advances fake timers but only flushes one microtask turn afterward. If the grace-period callback chains additional promises, the assertion can race the async cleanup and become flaky. Use the async timer helpers or wait for the observable state change after advancing time so the test fully drains the scheduled work.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:560:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • This is the same timer-drain pattern as the surrounding stale-timer tests. Advancing time without awaiting the async timer queue can leave the disconnect callback partially processed when the expectation runs. Prefer the async timer API so the test waits for the scheduled work to finish deterministically.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:571:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • The grace-period expiry path is time-based, but the test only advances the clock synchronously and then checks state after a single microtask. That can race the disconnect/reconnect cleanup on slower runners. Use async timer advancement or a condition-based wait after the timer fires.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:577:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs + 1000);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(
      +        PERPS_CONSTANTS.ConnectionGracePeriodMs + 1000,
      +      );
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • The assertion depends on the grace-period callback having fully completed. A synchronous timer advance plus one microtask flush is not always enough when the callback performs async work. Switching to the async timer helper makes the test deterministic.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:583:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • This test checks the post-timer connected state, so it should wait for the timer callback to settle rather than assuming one microtask is enough. Using the async timer API avoids intermittent failures when the callback schedules follow-up work.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:591:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • The stale-timer ignore path is also time-driven and should be drained with the async timer helpers. Otherwise the disconnect callback can still be pending when the expectation runs, especially under CI load.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:599:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • This test is especially sensitive because it expects the live connection to remain usable after the grace-period timer fires. Advancing time synchronously can leave the callback mid-flight; use the async timer API so the state is observed only after the scheduled work completes.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:607:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • The active-arm path also relies on the timer callback fully settling before the disconnect assertion. A synchronous advance plus microtask flush can race the callback on slower machines. Use async timer advancement to make the test deterministic.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:615:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • This timeout test advances fake timers but does not await the async timer queue before asserting the trace end. If the timeout handler performs promise-based cleanup, the expectation can race the callback. Use the async timer helper or wait for the expected endTrace call after advancing time.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:648:
      -      jest.advanceTimersByTime(120_000);
      +      await jest.advanceTimersByTimeAsync(120_000);
  • J2 — Real timers where fake timers are needed (high)
    • The session-timeout notification path is timer-driven, but the test only advances the clock synchronously. That can leave the listener notification pending when the assertion runs. Use the async timer API so the callback is fully processed before checking the spy.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:658:
      -      jest.advanceTimersByTime(90_000);
      +      await jest.advanceTimersByTimeAsync(90_000);
  • J2 — Real timers where fake timers are needed (high)
    • This stale-iOS-timer test depends on the grace-period callback being fully drained before the disconnect assertion. Advancing time synchronously can race the callback on CI. Use the async timer helper to make the timer completion deterministic.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:742:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
  • J2 — Real timers where fake timers are needed (high)
    • The inactive-edge timer path has the same race: the test advances time but does not wait for the async callback chain to finish. Use async timer advancement so the assertion observes the final state, not an intermediate one.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:748:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • The longer stale-timer case is also vulnerable to callback timing races. Advancing the clock synchronously is not enough if the disconnect logic schedules follow-up promises. Use the async timer helper to fully settle the timer work before asserting.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:754:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs + 1000);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(
      +        PERPS_CONSTANTS.ConnectionGracePeriodMs + 1000,
      +      );
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • The live-connection assertion after a stale timer is ignored should wait for the timer callback to settle. A synchronous advance can leave the callback mid-flight and make the state check flaky.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:760:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • The active-arm disconnect path should also use async timer advancement so the final disconnect assertion runs after the scheduled callback has completed, not while it is still pending.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:770:
      -      jest.advanceTimersByTime(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      -      await Promise.resolve();
      -      await Promise.resolve();
      +      await jest.advanceTimersByTimeAsync(PERPS_CONSTANTS.ConnectionGracePeriodMs);
      +      await Promise.resolve();
      +      await Promise.resolve();
  • J2 — Real timers where fake timers are needed (high)
    • This reconnect retry test advances fake timers synchronously. If the retry path performs promise-based work after the timeout, the assertion can race the callback. Use the async timer helper so the retry is fully processed before checking the result.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:839:
      -      jest.advanceTimersByTime(1000);
      +      await jest.advanceTimersByTimeAsync(1000);
  • J2 — Real timers where fake timers are needed (high)
    • The retry-success path has the same timing risk as the previous test. Advancing the clock synchronously can leave the reconnect callback unresolved when the expectation runs.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:848:
      -      jest.advanceTimersByTime(1000);
      +      await jest.advanceTimersByTimeAsync(1000);
  • J2 — Real timers where fake timers are needed (high)
  • J2 — Real timers where fake timers are needed (high)
  • J2 — Real timers where fake timers are needed (high)
    • The final retry test advances time synchronously and then immediately asserts on the connection state. That can be flaky if the retry callback schedules additional async work. Use the async timer helper instead.
    • Suggested fix in app/components/UI/Perps/services/PerpsConnectionManager.test.ts:875:
      -      jest.advanceTimersByTime(1000);
      +      await jest.advanceTimersByTimeAsync(1000);

app/components/UI/Perps/utils/perpsLoadingSession.test.ts

  • J2 — Real timers where fake timers are needed (fake timers leak between tests) (high)
    • Two tests call jest.useFakeTimers() inline without a matching jest.useRealTimers() in an afterEach. The jest.clearAllMocks() in beforeEach clears mock call counts but does NOT restore the timer implementation. As a result, fake timers remain active for every test that runs after these two — including tests in the same describe('finishPerpsLoadingSession') block that follow them. Any subsequent test that relies on real timer behavior (e.g. Date.now() returning wall-clock time, or any async operation that uses real setTimeout internally) will behave incorrectly or hang. The fix is to add afterEach(() => { jest.useRealTimers(); }) inside the describe('finishPerpsLoadingSession') block (or at the top-level describe) so timers are always restored after each test.
    • Suggested fix in app/components/UI/Perps/utils/perpsLoadingSession.test.ts:1:
      -  it('ends and releases a session at the bounded timeout', () => {
      -      jest.useFakeTimers();
      -      const startedAtWallMs = Date.now();
      -      startPerpsLoadingSession();
      -      ...
      -      jest.advanceTimersByTime(120_000);
      -      ...
      -  });
      -
      -  it('notifies subscribers when a session times out', () => {
      -      jest.useFakeTimers();
      -      const listener = jest.fn();
      -      ...
      -      jest.advanceTimersByTime(90_000);
      -      ...
      -  });
      -
      -  // No afterEach(() => jest.useRealTimers()) anywhere in the describe block
      +  // Add inside describe('finishPerpsLoadingSession') — or at the top-level describe:
      +  afterEach(() => {
      +    jest.useRealTimers();
      +  });
      +
      +  it('ends and releases a session at the bounded timeout', () => {
      +    jest.useFakeTimers();
      +    const startedAtWallMs = Date.now();
      +    startPerpsLoadingSession();
      +    // ... rest of test unchanged
      +  });
      +
      +  it('notifies subscribers when a session times out', () => {
      +    jest.useFakeTimers();
      +    const listener = jest.fn();
      +    // ... rest of test unchanged
      +  });
  • J10 — jest.spyOn() without restoreAllMocks()/mockRestore() afterward (medium)
    • jest.spyOn(Date, 'now').mockReturnValue(...) is called inside the 'ends and releases a session at the bounded timeout' test. The spy is stored in dateNow but dateNow.mockRestore() is never called, and there is no afterEach(() => jest.restoreAllMocks()) anywhere in the file. The jest.clearAllMocks() in beforeEach resets call counts but does NOT restore the original Date.now implementation. Consequently, every test that runs after this one will see a stubbed Date.now() returning a fixed value instead of the real wall-clock time. Tests that compare timestamps or use Date.now() for relative timing will produce wrong results or fail intermittently depending on test execution order. The fix is to add afterEach(() => { jest.restoreAllMocks(); }) — which can be combined with the jest.useRealTimers() fix from J2.
    • Suggested fix in app/components/UI/Perps/utils/perpsLoadingSession.test.ts:1:
      -  it('ends and releases a session at the bounded timeout', () => {
      -      jest.useFakeTimers();
      -      const startedAtWallMs = Date.now();
      -      startPerpsLoadingSession();
      -      const expectedDeadline = startedAtWallMs + 90_000;
      -      jest.mocked(performance.now).mockReturnValue(120_400);
      -      const dateNow = jest
      -        .spyOn(Date, 'now')
      -        .mockReturnValue(startedAtWallMs + 120_000);
      -      jest.advanceTimersByTime(120_000);
      -      expect(endTrace).toHaveBeenCalledWith(
      -      // ...
      -  });
      -
      -  // No afterEach(() => jest.restoreAllMocks()) in the file
      +  // Add inside describe('finishPerpsLoadingSession') — or at the top-level describe:
      +  afterEach(() => {
      +    jest.useRealTimers();
      +    jest.restoreAllMocks();
      +  });
      +
      +  it('ends and releases a session at the bounded timeout', () => {
      +    jest.useFakeTimers();
      +    const startedAtWallMs = Date.now();
      +    startPerpsLoadingSession();
      +    const expectedDeadline = startedAtWallMs + 90_000;
      +    jest.mocked(performance.now).mockReturnValue(120_400);
      +    const dateNow = jest
      +      .spyOn(Date, 'now')
      +      .mockReturnValue(startedAtWallMs + 120_000);
      +    jest.advanceTimersByTime(120_000);
      +    expect(endTrace).toHaveBeenCalledWith(
      +      // ... rest of test unchanged
      +    );
      +    // dateNow.mockRestore() is now handled by afterEach
      +  });

app/components/Views/TrendingView/feeds/perps/usePerpsFeed.test.ts

  • J1 — Wrong testing-library renderHook import causes missing act() wrapping (critical)
    • The file imports renderHook from the legacy @testing-library/react-hooks package instead of @testing-library/react-native. In the React Native + React 18 environment used by this codebase, the legacy package does not integrate with the React test renderer's act() boundary correctly. This causes intermittent 'not wrapped in act(...)' warnings that can escalate to test failures when hooks trigger state updates (e.g. via usePerpsMarkets, useSelector, or useHomepageSparklines). All other hook tests in this codebase (e.g. useHomepageSparklines.test.ts, usePerpsTrendingCarouselData.test.ts) correctly import from @testing-library/react-native. Switching to the canonical import eliminates the act() boundary mismatch.
    • Suggested fix in app/components/Views/TrendingView/feeds/perps/usePerpsFeed.test.ts:1:
      -import { renderHook } from '@testing-library/react-hooks';
      +import { renderHook } from '@testing-library/react-native';
  • J9 — Module-level mutable binding not reset in beforeEach (high)
    • The module-level mockMarkets array is captured by the jest.mock factory closure and used as the default return value for usePerpsMarkets. Individual tests override this via (usePerpsMarkets as jest.Mock).mockReturnValue(...), but jest.clearAllMocks() in beforeEach resets call counts and implementations — it restores usePerpsMarkets to its original factory, which still references the shared mockMarkets array. If any test were to push items into mockMarkets (e.g. mockMarkets.push(...)) rather than using mockReturnValue, those mutations would bleed into subsequent tests. While current tests do not mutate the array directly, the pattern is fragile: the factory closure creates a hidden shared-state dependency. The safer pattern is to always use mockReturnValue in beforeEach to set a fresh default, making the default state explicit and independent of the closure.
    • Suggested fix in app/components/Views/TrendingView/feeds/perps/usePerpsFeed.test.ts:22:
      -const mockMarkets: PerpsMarketData[] = [];
      -const mockRefetch = jest.fn();
      -
      -jest.mock('../../../../UI/Perps/hooks', () => ({
      -  usePerpsMarkets: jest.fn(() => ({
      -    markets: mockMarkets,
      -    isLoading: false,
      -    refresh: mockRefetch,
      -    isRefreshing: false,
      -  })),
      -}));
      +const mockRefetch = jest.fn();
      +
      +jest.mock('../../../../UI/Perps/hooks', () => ({
      +  usePerpsMarkets: jest.fn(),
      +}));
      +
      +// In beforeEach, after jest.clearAllMocks():
      +beforeEach(() => {
      +  jest.clearAllMocks();
      +  mockFuseSearch.mockImplementation((items: unknown[]) => items);
      +  jest.mocked(usePerpsMarkets).mockReturnValue({
      +    markets: [],
      +    isLoading: false,
      +    refresh: mockRefetch,
      +    isRefreshing: false,
      +  });
      +});

app/util/trace.test.ts

  • J10 — jest.spyOn without restoreAllMocks() (medium)
    • This test installs a spy on Date.now but does not restore it in the test body. Although the file has a global afterEach restore, this case is still risky because the spy is created in a section that also mutates shared performance state; if the test throws before the global cleanup runs, the spy can leak into later tests and change timing behavior. Restoring the spy in a finally block keeps the test isolated even on early failure.
    • Suggested fix in app/util/trace.test.ts:496:
      -    it('defaults the span end time to the performance clock, not the wall clock', () => {
      -      updateCachedConsent(true);
      -      const dateNowSpy = jest.spyOn(Date, 'now');
      -      performanceMock.now.mockReturnValue(750);
      -      const spanEndMock = jest.fn();
      -      const spanMock = {
      -        end: spanEndMock,
      -        setStatus: jest.fn(),
      -        setAttribute: jest.fn(),
      -      } as unknown as Span;
      +    it('defaults the span end time to the performance clock, not the wall clock', () => {
      +      updateCachedConsent(true);
      +      const dateNowSpy = jest.spyOn(Date, 'now');
      +      performanceMock.now.mockReturnValue(750);
      +      const spanEndMock = jest.fn();
      +      const spanMock = {
      +        end: spanEndMock,
      +        setStatus: jest.fn(),
      +        setAttribute: jest.fn(),
      +      } as unknown as Span;
      +
      +      try {
      +        trace({ name: NAME_MOCK });
      +
      +        expect(spanEndMock).toHaveBeenCalled();
      +      } finally {
      +        dateNowSpy.mockRestore();
      +        endTrace({ name: NAME_MOCK });
      +      }
      +    });

This check is informational only and does not block merging.

pull Bot pushed a commit to Reality2byte/metamask-mobile that referenced this pull request Aug 19, 2026
## **Description**

Uses Perps Controller 12 global-market and atomic-user snapshots to make
Homepage Perps data available earlier while preserving the
direct-provider fallback. Requests are scoped to exact provider,
network, DEX, and account identity. Account switches clear only user
state and preserve account-independent markets and prices.

Measured physical-device evidence:

| Cohort | Current main | Candidate | Result |
|---|---:|---:|---|
| Android cold markets ready | 16.97s | 3.25s | 80.8% faster on
Terminal-v2 hits; n=3/arm, hardware unmatched |
| Android resolved Homepage Perps | 18.27s | 2.15s | 88.2% faster on
Terminal-v2 hits |
| iOS dev1 → empty dev2 | 2.42s | 2.63s | 3/3 correct first paints in
both arms; no demonstrated regression |

Terminal adoption was 3/4 before terminal-backend#49. Direct-provider
fallback remains the correctness path and passed the post-rebase iOS
smoke. Production instrumentation follows separately in MetaMask#34948.

## **Changelog**

CHANGELOG entry: Improved Perps market and account loading on the wallet
homepage

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/TAT-3662

## **Manual testing steps**

```gherkin
Feature: Perps homepage bootstrap

  Scenario: switch from an account with a position to an empty account
    Given the wallet is unlocked on dev1 and Homepage Perps shows its position
    When the perps.performance recipe switches to dev2
    Then no dev1 position is shown for dev2
    And account-independent markets and prices remain available
    And dev2 resolves to an empty positions state

  Scenario: Terminal v2 is unavailable
    Given the wallet is unlocked and Terminal v2 returns an error
    When Homepage Perps loads
    Then the direct provider fallback supplies market data
```

Recipe:
`perps.performance lifecycle=account_switch initial_account=dev1
account=dev2 initial_content_variant=positions content_variant=trending`

Validation completed:
- dev1 position → empty dev2: 3/3 correct candidate and 3/3 control.
- Global markets remained populated during account-only switching.
- Post-rebase iOS smoke: 38/38 checks passed.
- Focused product suites and complete ConnectionManager suite passed.

## **Screenshots/Recordings**

### **Before**

N/A — performance and state transitions are captured by the reusable
recipe evidence.

### **After**

N/A — performance and state transitions are captured by the reusable
recipe evidence.

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

#### Performance checks (if applicable)

- [x] I've tested on Android
  - Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example

## **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.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes Perps streaming, Terminal routing, and account-switch cache
behavior where stale positions or mixed market sources would be
user-visible; mitigated by identity checks, snapshot coalescing, and
direct-provider fallback.
> 
> **Overview**
> **Faster homepage Perps bootstrap** by wiring Perps Controller
**Terminal v2 global market snapshots** and a single **atomic user
snapshot** (positions, orders, account) into `PerpsStreamManager`, with
v1 market URLs kept for legacy enrichment. Mobile infra now passes
`terminalApi.marketDataUrl` + `terminalApi.globalSnapshotUrl`
(env-derived v2 host, dev-only `MM_PERPS_TERMINAL_GLOBAL_SNAPSHOT_URL`
override).
> 
> **Market data channel** treats Hyperliquid + configured v2 URL as a
distinct source: can fetch **standalone** before WS readiness, only
adopts controller cache when every row is
`terminal-global-snapshot-mark`, and invalidates when
provider/network/terminal vs global-snapshot mode changes so
direct/Terminal data is not mixed.
> 
> **User streams** coalesce on subscribe via `getUserDataSnapshot()`
(Hyperliquid only), seed all three channels together, and ignore stale
or in-flight snapshots after cache clear or newer live delivery. Channel
caches/snapshots are **account-address scoped** so the wrong wallet’s
data is not served.
> 
> **Account switches** clear only user-scoped state in
`PerpsConnectionManager` while keeping global markets/prices; live hooks
(`usePerpsLivePositions`, orders, account) immediately enter loading and
hide data when the selected address changes.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f069a8e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.93506% with 124 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.36%. Comparing base (5010639) to head (1d14c19).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
...p/components/UI/Perps/utils/perpsLoadingSession.ts 84.78% 12 Missing and 23 partials ⚠️
...ents/UI/Perps/hooks/usePerpsMarketDetailSession.ts 92.46% 4 Missing and 7 partials ⚠️
.../PerpsMarketDetailsView/PerpsMarketDetailsView.tsx 85.24% 3 Missing and 6 partials ⚠️
...Perpetuals/hooks/useHomepagePerpsSurfaceMetrics.ts 89.15% 5 Missing and 4 partials ⚠️
app/util/trace.ts 75.86% 4 Missing and 3 partials ⚠️
...ponents/UI/Perps/utils/perpsLoadingSessionModel.ts 53.84% 2 Missing and 4 partials ⚠️
...Sections/Perpetuals/hooks/useHomepageSparklines.ts 91.30% 1 Missing and 5 partials ⚠️
...onents/UI/Perps/services/PerpsConnectionManager.ts 90.00% 1 Missing and 4 partials ⚠️
...Perpetuals/hooks/usePerpsHomepageLoadingSession.ts 94.73% 2 Missing and 3 partials ⚠️
...UI/Perps/providers/channels/CandleStreamChannel.ts 87.50% 2 Missing and 2 partials ⚠️
... and 14 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #34948      +/-   ##
==========================================
+ Coverage   85.31%   85.36%   +0.05%     
==========================================
  Files        6584     6598      +14     
  Lines      179351   180581    +1230     
  Branches    44527    44894     +367     
==========================================
+ Hits       153014   154158    +1144     
- Misses      16088    16110      +22     
- Partials    10249    10313      +64     

☔ 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.

Comment thread app/components/UI/Perps/hooks/usePerpsMarketDetailSession.ts
pull Bot pushed a commit to dmrazzy/core that referenced this pull request Aug 24, 2026
…9939)

## Explanation

Candle cleanup currently drops the promise returned by the HyperLiquid
SDK unsubscribe call. A rejected cleanup therefore becomes an unhandled
promise rejection during Mobile foreground recovery.

This change routes resolved and pending cleanup through one idempotent
helper. It consumes async rejections, catches synchronous throws,
ignores the SDK's already-unsubscribed result, and logs unexpected
failures.

## References

- Related to MetaMask/metamask-mobile#34948
- Mobile uses a temporary Yarn patch until this fix is released.

## Validation

- Focused HyperLiquidClientService suite: 90/90 passed
- Changed-file ESLint and formatting passed
- Perps controller changelog validation passed
- The standalone package build requires prebuilt workspace dependency
outputs in a fresh checkout; CI will run the complete build graph.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation as appropriate
- [x] I've communicated my changes to consumers by updating the package
changelog
- [ ] I've introduced breaking changes in this PR

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Defensive error handling on candle subscription teardown only; no
trading, auth, or data-path changes.
> 
> **Overview**
> Stops HyperLiquid candle cleanup from dropping the SDK `unsubscribe()`
promise, which could become an unhandled rejection (notably during
Mobile foreground recovery).
> 
> `subscribeToCandles` now routes resolved and pending teardown through
one idempotent helper: it consumes async rejections, catches sync
throws, ignores the SDK's "Already unsubscribed" result, and logs
unexpected failures. Tests cover both the logged failure path and the
idempotent late-cleanup path.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
377bfed. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Comment thread app/components/UI/Perps/utils/perpsLoadingSession.ts
Comment thread app/components/UI/Perps/providers/PerpsStreamManager.tsx
Comment thread app/components/UI/Perps/hooks/usePerpsMarketDetailSession.ts Outdated

@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 c4b5fda. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeAccounts, SmokeConfirmations, SmokeNetworkAbstractions, SmokeNetworkExpansion, SmokeSwap, SmokeStake, SmokeWalletPlatform, SmokeMoney, SmokePerps, SmokeMultiChainAPI, SmokePredictions, SmokeSeedlessOnboarding, SmokeBrowser, SmokeSnaps, SmokeMMConnect
  • Selected Performance tags: @PerformancePreps
  • Risk Level: high
  • AI Confidence: 100%
click to see 🤖 AI reasoning details

E2E Test Selection:
Hard rule (controller-version-update): @MetaMask controller package version updated in package.json: @metamask/perps-controller. Running all tests.

Performance Test Selection:
The @PerformancePreps performance tests directly measure perps market loading, position management, add funds flow, and order execution. This PR makes significant changes to: (1) the perps-controller dependency (12.0.0 → 12.1.0 patched), (2) the entire loading session architecture which tracks time-to-content for Perps screens, (3) PerpsConnectionManager connection generation tracking which affects reconnection timing, (4) PerpsStreamManager subscription generation tracking, and (5) the Add Funds flow which is directly tested in tests/performance/login/perps-add-funds.spec.ts. The new loading session system is specifically designed to measure performance milestones, making performance test validation essential to verify the new instrumentation doesn't introduce regressions.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Performance Test Results

ℹ️ Performance test results are currently non-blocking and will not block this PR.

1 test failed · 2 tests · 1 device

📱 Devices tested (1)

Android: Google Pixel 8 Pro (v14.0)

❌ Failed Tests (1)

@mm-perps-engineering-team

Perps open position and close it

Platform Device Reason Recording
Android Google Pixel 8 Pro (v14.0) no_performance_metrics 📹 Watch
✅ Passed Tests (1)
Test Platform Device Duration Team Recording
Perps add funds Android Google Pixel 8 Pro (v14.0) 8.47s @mm-perps-engineering-team 📹 Watch

Branch: perf/perps-loading-session-stack · Build: E2E · Commit: c73300b · View full run

geositta
geositta previously approved these changes Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-performance Issues relating to slowness of app, cpu usage, and/or blank screens. risk:high AI analysis: high risk size-XL team-perps Perps team type-enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants