Skip to content

Conversation

@MrFlashAccount
Copy link

@MrFlashAccount MrFlashAccount commented Oct 27, 2025

This PR is closed in favor of the #9827

Outdated description

PR is ready for a review/discussion. Once I address all issues, will create changesets.

🎯 Changes

TLDR: this PR improves mount/subscription performance by 5-10 times by using a single GC interval instead of a timeout per query.

This PR does not introduce any breaking changes, as edge cases behave exactly the same and the other cases are garbage collected within [gcTime, gcTime + 10ms] interval.

Demo: https://react-19-query-demo-git-tanstac-d7ec69-mrflashaccounts-projects.vercel.app/

Performance difference Before: CleanShot 2025-10-27 at 12 53 34@2x

After:
CleanShot 2025-10-27 at 12 52 46@2x


Long explanation

Open to view

Overview

This PR refactors TanStack Query's garbage collection from individual timeout-based approach to a unified periodic scan approach, dramatically improving performance and scalability for applications with many queries.

The Problem

Before (Timeout-Based)

  • Creating timers: Each query/mutation schedules its own setTimeout(gcTime) which is expensive.
  • O(n) timers: Each query/mutation schedules its own setTimeout(gcTime)
  • Event loop congestion: N timers competing in the event loop
  • Scalability issues: Apps with 1000+ queries create 1000+ timers

Performance Impact

  • 100 inactive queries → 100 timers
  • 1000 inactive queries → 1000 timers
  • 10000 inactive queries → 10000 timers

The Solution

After (Periodic Scan)

  • O(1) scan: Single global interval (10ms default) scans all caches
  • Predictable: Regular scan intervals instead of scattered timeouts
  • Scalable: Works equally well with 10 or 10,000 queries
// Single global timer scans all queries
query.removeObserver()  mark gcEligibleAt timestamp
gcManager.interval  scan all caches  remove eligible queries

Performance Improvement

  • 100 inactive queries → 1 timer
  • 1000 inactive queries → 1 timer
  • 10000 inactive queries → 1 timer

Scan Interval Configuration

Users can now configure the scan interval:

import { GCManager } from '@tanstack/query-core'

// Default: 10ms (near real-time GC)
const queryClient = new QueryClient()

// Custom interval: 100ms (more CPU usage, less memory)
const queryClient = new QueryClient({
  gcManager: new GCManager({ scanInterval: 100 }),
})

// Relaxed: 60s (lower CPU, higher memory)
const queryClient = new QueryClient({
  gcManager: new GCManager({ scanInterval: 60000 }),
})

Behavioral Change

GC Timing Precision

Before: Items removed after gcTime expiration (at gcTime + event loop delay)
After: Items removed within scanInterval after becoming eligible (at gcTime + scanInterval + event loop delay)

Example (gcTime: 5 minutes, scanInterval: 10ms):

  • Before: GC at exactly 5:00:00.000
  • After: GC between 5:00:00.000 and 5:00:00.010

In practice, this 10ms delay is negligible for most applications.

Immediate GC

For gcTime: 0, queries trigger an immediate scan:

// gcTime: 0 → scheduled immediate scan
if (gcTime === 0 && isSafeToRemove()) {
  gcManager.scheduleImmediateScan()
}

Timer Reduction

For an app with 1000 inactive queries:

  • Before: 1000 timers
  • After: 1 timer
  • Reduction: 99.9%

Scalability

  • Works efficiently with 10, 100, 1000, or 10000 queries
  • Constant overhead regardless of cache size
  • Predictable performance characteristics

API Compatibility

✅ No Breaking Changes

  • Public API remains unchanged
  • All hooks, methods, and options work the same
  • gcTime option behavior is preserved (within 10ms). 0 and Infinity are unchanged.

Benefits

Performance

  • ✅ 99% fewer timers in typical apps
  • ✅ Better scalability for large caches
  • ✅ Predictable GC timing

Reliability

  • ✅ Less event loop congestion
  • ✅ Simpler debugging (one timer)
  • ✅ No timer leaks

Trade-offs

  • Items held in memory up to scanInterval longer (10ms default)

Migration Guide

For Library Users

No changes required! Existing code continues to work.

Future Enhancements

  • Collect only unused queries by using another map to track unused queries

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

Release Notes

  • Refactor
    • Reorganized garbage collection system with centralized scheduling across caches for improved cleanup efficiency
    • Migrated to timestamp-based eligibility tracking for more accurate cache expiration management

@changeset-bot
Copy link

changeset-bot bot commented Oct 27, 2025

🦋 Changeset detected

Latest commit: 7dd1e63

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@tanstack/query-core Patch
@tanstack/angular-query-experimental Patch
@tanstack/query-async-storage-persister Patch
@tanstack/query-broadcast-client-experimental Patch
@tanstack/query-persist-client-core Patch
@tanstack/query-sync-storage-persister Patch
@tanstack/react-query Patch
@tanstack/solid-query Patch
@tanstack/svelte-query Patch
@tanstack/vue-query Patch
@tanstack/angular-query-persist-client Patch
@tanstack/react-query-persist-client Patch
@tanstack/solid-query-persist-client Patch
@tanstack/svelte-query-persist-client Patch
@tanstack/react-query-devtools Patch
@tanstack/react-query-next-experimental Patch
@tanstack/solid-query-devtools Patch
@tanstack/svelte-query-devtools Patch
@tanstack/vue-query-devtools Patch

Not sure what this means? Click here to learn what changesets are.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Warning

Rate limit exceeded

@MrFlashAccount has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 8 minutes and 14 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b030a69 and 7dd1e63.

📒 Files selected for processing (2)
  • .changeset/smart-crabs-flow.md (1 hunks)
  • packages/query-core/src/__tests__/gcManager.test.tsx (1 hunks)

Walkthrough

Introduces a timestamp-based GC system: new GCManager centralizes scheduling; Removable switched from timers to gcTime/gcMarkedAt; QueryClient, Query, and Mutation integrate GCManager; tests updated to assert timestamp-based GC marks and adjust minor timings.

Changes

Cohort / File(s) Summary
GC Manager Infrastructure
packages/query-core/src/gcManager.ts
New GCManager and GCManagerConfig exports: centralized tracking of eligible Removable items, microtask-based scheduling, timeout scheduling based on earliest gcTime, safe per-item GC invocation, force-disable flag, start/stop/clear API.
Removable Base Class
packages/query-core/src/removable.ts
New Removable implementation using gcTime and gcMarkedAt. Adds markForGc(), clearGcMark(), isEligibleForGc(), getGcAtTimestamp(), updateGcTime(), and abstracts getGcManager() and optionalRemove(): boolean. Removes previous timer fields/methods.
QueryClient Integration
packages/query-core/src/queryClient.ts
Initializes a GCManager (respecting server mode), exposes getGcManager(): GCManager, and includes GCManager cleanup in clear().
Query Integration
packages/query-core/src/query.ts
Adds private #gcManager, getGcManager() accessor, isSafeToRemove() helper, and switches calls from scheduling/clearing timeouts to markForGc()/clearGcMark(). optionalRemove() changed to public optionalRemove(): boolean.
Mutation Integration
packages/query-core/src/mutation.ts
Adds getGcManager() accessor, isSafeToRemove() helper, replaces scheduleGc()/clearGcTimeout() with markForGc()/clearGcMark(), updates observer lifecycle and state transitions to use timestamp-based GC marking.
GC Manager Tests
packages/query-core/src/__tests__/gcManager.test.tsx
New comprehensive tests covering initialization, tracking, scheduling, rescheduling, edge cases, error handling, stop/clear behavior, and interactions with mixed items.
React Query: useQuery tests
packages/react-query/src/__tests__/useQuery.test.tsx
Replace timer spies with assertions on gcMarkedAt, use vi.setSystemTime() and sleep to validate GC marking and eviction; use timestamp-based durations.
Solid Query: useQuery tests
packages/solid-query/src/__tests__/useQuery.test.tsx
Replace timer spies with gcMarkedAt checks, add fake timers/system time manipulation, and validate eviction via timestamp progression.
Mutation Tests (timing tweaks)
packages/query-core/src/__tests__/mutationCache.test.tsx, packages/react-query/src/__tests__/useMutation.test.tsx, packages/solid-query/src/__tests__/useMutation.test.tsx
Minor test timing adjustments (advance time from 10ms → 11ms) to align with new GC scheduling semantics.

Sequence Diagram(s)

sequenceDiagram
    participant R as Removable (Query/Mutation)
    participant GM as GCManager
    participant TM as Timer/TimeoutManager
    participant Micro as MicrotaskQueue

    R->>R: markForGc() (set gcMarkedAt)
    R->>GM: trackEligibleItem(this)
    GM->>Micro: enqueue scheduleScan (if not already)
    Micro->>GM: run #scheduleScan()
    GM->>GM: compute min(getGcAtTimestamp - now)
    GM->>TM: setTimeout(#performScan, delay)
    TM-->>GM: timeout fires
    GM->>GM: #performScan()
    loop each eligible item
        GM->>R: isEligibleForGc()
        alt eligible
            R-->>GM: true
            GM->>R: optionalRemove()
            R-->>GM: removed
            GM->>GM: untrack item
        else not eligible
            R-->>GM: false
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Pay extra attention to:
    • packages/query-core/src/gcManager.ts — microtask scheduling, reschedule/dedup logic, and timeout interactions.
    • packages/query-core/src/removable.ts — correctness of timestamp math and eligibility conditions (Infinity handling, edge values).
    • Integration points in query.ts and mutation.ts — consistent use of isSafeToRemove() and correct visibility/behavioral changes for optionalRemove().

Possibly related PRs

Suggested reviewers

  • TkDodo

Poem

🐰 Hop, hop, I mark the spot in time,
No ticking clocks — just timestamps chime,
A manager hums, the cleanup queue,
I track, I mark, then bid adieu,
Clean fields of cache — a rabbit's rhyme. ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description includes a comprehensive 🎯 Changes section with detailed explanation of the problem, solution, performance metrics, configuration options, behavioral changes, and API compatibility. The ✅ Checklist is properly filled with both items confirmed (Contributing guide followed and tests run locally). However, the 🚀 Release Impact section is incomplete—neither of the two mutually exclusive options is checked. Since this PR affects published code (new GCManager class, modifications to Query and Mutation classes, etc.), one of these options must be selected. The author has transparently indicated that changesets will be prepared after addressing outstanding issues, but this remains incomplete in the current submission. Before merging, the Release Impact section must be completed. The author should either check the first option and generate a changeset documenting this release impact, or confirm that the change is docs/CI/dev-only only (which does not appear applicable given the scope of changes to published query-core code). The author has already committed to preparing changesets after addressing issues, so this step should be completed prior to final merge approval.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Improve subscription performance by 10-20 times" directly describes the primary objective of the changeset. The pull request introduces a comprehensive GCManager system that refactors garbage collection from individual timeout-based approach to a unified periodic scan approach, which is fundamentally a performance improvement initiative. The title is specific, clear, and accurately reflects the main benefit and motivation of the changes across multiple files in the codebase.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@MrFlashAccount MrFlashAccount changed the title Improve-gc-performance Improve side-effects performance by 5-10 times Oct 27, 2025
@MrFlashAccount MrFlashAccount marked this pull request as ready for review October 27, 2025 10:09
@MrFlashAccount MrFlashAccount changed the title Improve side-effects performance by 5-10 times Improve Mount side-effects performance by 5-10 times Oct 27, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/solid-query/src/__tests__/useQuery.test.tsx (1)

3953-3966: Make GC timestamp assertion deterministic without fake timers

vi.setSystemTime usually requires fake timers; here timers stay real, so Date.now used by markForGc won’t follow your set time. Stub Date.now instead and drop useRealTimers.

Apply:

-    vi.setSystemTime(new Date(1970, 0, 1, 0, 0, 0, 0))
+    const fixedNow = new Date(1970, 0, 1, 0, 0, 0, 0).getTime()
+    const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(fixedNow)

     rendered.unmount()

     expect(query!.gcEligibleAt).not.toBeNull()
     expect(query!.gcEligibleAt).toBe(
       new Date(1970, 0, 1, 0, 0, 0, gcTime).getTime(),
     )
-
-    vi.useRealTimers()
+    nowSpy.mockRestore()

     await vi.waitFor(() => {
       return queryClient.getQueryCache().find({ queryKey: key }) === undefined
     })
packages/query-core/src/mutationCache.ts (1)

252-259: Avoid mutating the Set while iterating it

optionalRemove() can delete from #mutations; iterating the same Set risks skipping elements. Iterate a snapshot.

-  performGarbageCollection(): void {
-    for (const mutation of this.#mutations) {
+  performGarbageCollection(): void {
+    for (const mutation of Array.from(this.#mutations)) {
       if (mutation.isEligibleForGc()) {
         mutation.optionalRemove()
       }
     }
   }
packages/query-core/src/gcManager.ts (1)

134-147: Operational refinements to reduce idle overhead

  • Auto-stop scanning when no caches registered; restart on first register.
  • Optional: adapt scan interval based on cache size/load.
   startScanning(): void {
     if (this.#isScanning || isServer) {
       return
     }
@@
     this.#intervalId = timeoutManager.setInterval(() => {
-      if (!this.#isPaused) {
+      if (!this.#isPaused) {
         this.#performScan()
+        if (this.#caches.size === 0) {
+          this.stopScanning()
+        }
       }
     }, this.#scanInterval)
   }

Also applies to: 199-201

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38b4008 and 368be79.

📒 Files selected for processing (13)
  • packages/query-core/src/__tests__/mutationObserver.test.tsx (2 hunks)
  • packages/query-core/src/gcManager.ts (1 hunks)
  • packages/query-core/src/index.ts (2 hunks)
  • packages/query-core/src/mutation.ts (7 hunks)
  • packages/query-core/src/mutationCache.ts (3 hunks)
  • packages/query-core/src/query.ts (10 hunks)
  • packages/query-core/src/queryCache.ts (3 hunks)
  • packages/query-core/src/queryClient.ts (5 hunks)
  • packages/query-core/src/removable.ts (2 hunks)
  • packages/query-core/src/types.ts (2 hunks)
  • packages/react-query/src/__tests__/useQuery.test.tsx (2 hunks)
  • packages/react-query/src/__tests__/useSuspenseQueries.test.tsx (1 hunks)
  • packages/solid-query/src/__tests__/useQuery.test.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
packages/solid-query/src/__tests__/useQuery.test.tsx (1)
packages/solid-query/src/useQuery.ts (1)
  • useQuery (36-50)
packages/query-core/src/removable.ts (1)
packages/query-core/src/utils.ts (1)
  • isValidTimeout (93-95)
packages/query-core/src/mutationCache.ts (3)
packages/vue-query/src/mutationCache.ts (1)
  • MutationCache (10-25)
packages/query-core/src/subscribable.ts (1)
  • Subscribable (1-30)
packages/query-core/src/gcManager.ts (1)
  • GarbageCollectable (8-13)
packages/query-core/src/queryCache.ts (3)
packages/query-core/src/subscribable.ts (1)
  • Subscribable (1-30)
packages/query-core/src/gcManager.ts (1)
  • GarbageCollectable (8-13)
packages/query-core/src/queryObserver.ts (1)
  • query (704-721)
packages/query-core/src/mutation.ts (2)
packages/query-core/src/gcManager.ts (1)
  • GCManager (60-247)
packages/query-core/src/index.ts (1)
  • GCManager (17-17)
packages/react-query/src/__tests__/useQuery.test.tsx (2)
packages/query-core/src/queryObserver.ts (1)
  • query (704-721)
packages/react-query/src/__tests__/utils.tsx (1)
  • renderWithClient (9-23)
packages/query-core/src/gcManager.ts (2)
packages/query-core/src/index.ts (5)
  • GCManagerConfig (53-53)
  • GCManager (17-17)
  • ManagedTimerId (25-25)
  • isServer (31-31)
  • timeoutManager (24-24)
packages/query-core/src/timeoutManager.ts (1)
  • systemSetTimeoutZero (133-135)
packages/query-core/src/types.ts (2)
packages/query-core/src/gcManager.ts (1)
  • GCManager (60-247)
packages/query-core/src/index.ts (1)
  • GCManager (17-17)
packages/query-core/src/query.ts (2)
packages/query-core/src/gcManager.ts (1)
  • GCManager (60-247)
packages/query-core/src/index.ts (1)
  • GCManager (17-17)
packages/query-core/src/queryClient.ts (2)
packages/query-core/src/gcManager.ts (1)
  • GCManager (60-247)
packages/query-core/src/index.ts (1)
  • GCManager (17-17)
🔇 Additional comments (16)
packages/query-core/src/__tests__/mutationObserver.test.tsx (1)

61-61: LGTM! Timer adjustments align with new GC timing model.

The doubled wait time (10ms → 20ms) correctly accounts for the new GC timing behavior: gcTime (10ms) + scanInterval (10ms default). Under the centralized GC system, items become eligible at gcTime but are removed during the next periodic scan.

Also applies to: 83-83

packages/react-query/src/__tests__/useSuspenseQueries.test.tsx (1)

651-656: LGTM! Test correctly updated for new GC timing and verification.

Two appropriate changes:

  1. Timer adjustment: 1000ms → 1010ms accounts for gcTime (1000ms) + scanInterval (10ms).
  2. Verification method: Checking cache.find() directly is more accurate for GC verification than getQueryData, as it confirms cache-level removal rather than just data absence.
packages/query-core/src/removable.ts (5)

3-21: LGTM! Clear documentation and appropriate field types.

The JSDoc clearly explains the shift from individual timeouts to timestamp-based GC eligibility. The gcEligibleAt: number | null type correctly models the tri-state: not marked (null), marked with timestamp (number), or Infinity gcTime (remains null).


26-47: LGTM! Correct GC mark implementation.

The markForGc() method correctly handles all gcTime scenarios:

  • Valid finite gcTime: sets future timestamp
  • Infinity: sets null (never eligible)
  • gcTime: 0: sets Date.now() (immediately eligible, triggers immediate scan per PR description)

56-76: LGTM! Eligibility check implementation is correct.

isEligibleForGc() properly determines eligibility by comparing current time against the marked timestamp. The null check ensures unmarked items and those with gcTime: Infinity are never collected.


83-105: LGTM! Time calculation and update logic are sound.

getTimeUntilGc() correctly calculates remaining time with proper null handling and negative-to-zero clamping. updateGcTime() appropriately uses Math.max() to honor the "longest gcTime wins" rule documented earlier, with sensible defaults (5min client / Infinity server).


107-116: LGTM! Appropriate abstraction and visibility change.

Making optionalRemove() public and abstract is correct: the GCManager and cache implementations need to call this method from outside the class hierarchy, and subclasses (Query, Mutation) provide the specific removal logic.

packages/query-core/src/types.ts (1)

6-6: LGTM! Clean API extension with backward compatibility.

The optional gcManager field in QueryClientConfig enables custom GCManager injection while maintaining full backward compatibility. Type-only import is appropriate.

Also applies to: 1355-1355

packages/query-core/src/index.ts (1)

17-17: LGTM! Appropriate public API exposure.

Exporting GCManager and GCManagerConfig enables external configuration and custom GC management while following the package's established export patterns. No breaking changes introduced.

Also applies to: 53-53

packages/query-core/src/queryClient.ts (3)

15-15: LGTM! Consistent initialization pattern.

GCManager initialization follows the established pattern for queryCache and mutationCache, using the config value or creating a default instance. The private field encapsulation is appropriate.

Also applies to: 63-63, 76-76


82-84: LGTM! Proper cache registration and scan initialization.

Both caches are registered with GCManager and scanning starts automatically. The automatic start is safe because GCManager.startScanning() includes an isServer guard that prevents scanning on SSR.


464-466: LGTM! Consistent accessor pattern.

The getGcManager() method follows the established pattern of getQueryCache() and getMutationCache(), enabling external access to GC controls for advanced use cases and testing.

packages/query-core/src/queryCache.ts (3)

16-16: LGTM! Proper interface implementation.

QueryCache correctly implements the GarbageCollectable interface, enabling integration with the centralized GCManager system. Type-only import is appropriate.

Also applies to: 93-96


235-242: LGTM! Correct GC implementation.

performGarbageCollection() properly iterates eligible queries and delegates removal to query.optionalRemove(), which handles the actual removal and notifications through the existing remove() path.


247-250: LGTM! Proper cleanup implementation.

The destroy() method correctly delegates to clear(), which handles batch notification and resource cleanup for all queries. This provides a clean shutdown path for the cache.

packages/query-core/src/query.ts (1)

374-379: GC integration verified—all requirements confirmed

QueryClient correctly registers both QueryCache and MutationCache with GCManager and immediately starts scanning on initialization (queryClient.ts:82–84). Query and Mutation classes properly access GCManager through getGcManager(). The marking logic and immediate scans for gcTime=0 are consistent with the registration flow.

@MrFlashAccount MrFlashAccount changed the title Improve Mount side-effects performance by 5-10 times Improve Mount side-effects performance by 10-20 times Oct 27, 2025
@MrFlashAccount MrFlashAccount changed the title Improve Mount side-effects performance by 10-20 times Improve subscription performance by 10-20 times Oct 27, 2025
@timdorr
Copy link

timdorr commented Oct 27, 2025

Random observer here! 👋 I'm not seeing where the GC ever stops running. This would be running every 10ms for the life of the application, even after everything has been GC'ed or is settled. That might keep the page active and prevent some backgrounding behaviors for inactive pages/tabs. There should probably be some settling behavior or backoff for the sweeps.

@nx-cloud
Copy link

nx-cloud bot commented Oct 27, 2025

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 4c7f288

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ❌ Failed 3m 38s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 19s View ↗

☁️ Nx Cloud last updated this comment at 2025-10-27 19:29:33 UTC

@MrFlashAccount
Copy link
Author

@timdorr Hi!

That's a good observation. Thank you. Initially, implementation was to start the GC manager on mount and stop on unmount. But it works only if you mount the queryClient using the QueryClientProvider. I think you concern makes sense. Will try to find out a solution for this. One of the solutions could be to store queries to collect in a map, and start the timer only when map.size > 0.

…ss caches

- Improved performance by 5-10 times
- Tests left intact (no breaking changes expected)
- Added `scheduleImmediateScan` method to `GCManager` for immediate garbage collection scans.
- Integrated `GCManager` into `Mutation` and `Query` classes to trigger immediate scans when conditions are met.
- Updated tests to verify new garbage collection behavior.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/query-core/src/mutation.ts (2)

150-153: removeObserver: GC mark only when safe — matches guidance

Addresses the earlier concern about scheduling scans while pending.


386-389: Dispatch-time GC mark without early‑return is correct

Keeps notifications consistent while still enabling GC when safe.

🧹 Nitpick comments (7)
packages/query-core/src/__tests__/query.test.tsx (1)

561-562: Double 0ms advances to flush GC scheduling

Good call adding two 0ms advances to flush the microtask that schedules the timeout and the timeout itself. Consider a tiny helper (e.g., flushGcScheduling) to avoid repetition across tests.

packages/query-core/src/queryClient.ts (1)

64-82: GCManager integration and SSR gating look correct

  • Initializing GCManager with forceDisable: isServer prevents clientless environments from scheduling timers.
  • Public getGcManager accessor and clear() cleanup are appropriate.

Optional: allow injecting a custom GCManager via QueryClientConfig for advanced testing or alternative strategies.

Also applies to: 461-463, 656-657

packages/query-core/src/__tests__/gcManager.test.tsx (1)

49-56: Harden scheduling assertions to avoid flakiness

GC scheduling uses a microtask to schedule the timeout. In places you assert isScanning() right after unsubscribe, use a helper to flush both microtask and the potential 0ms timeout:

+async function flushGcScheduling() {
+  await Promise.resolve();        // flush microtasks
+  await vi.advanceTimersByTimeAsync(0); // run any 0ms timers
+}

Then replace single 0ms advances like:

- await vi.advanceTimersByTimeAsync(0)
+ await flushGcScheduling()

This reduces flakes across environments.

Also applies to: 71-76, 101-124, 160-169, 198-207, 255-265, 286-296

packages/query-core/src/removable.ts (1)

40-48: Mark-for-GC logic is correct; consider unified time source

Using Date.now() is fine, but if getTimeUntilGc() relies on a different clock, consider centralizing through a shared time utility to avoid drift in tests or non-DOM runtimes.

packages/query-core/src/gcManager.ts (3)

49-95: On‑demand scheduling fixes “always‑on interval” concern

Microtask + min(gcAt) timeout = no continuous timer; nothing runs when no eligibles. This addresses background tab/idle concerns.

Consider documenting this deviation from “global interval” in the PR description/docs so expectations match behavior.


145-163: Minor: cancel pending microtask when set becomes empty and no active timer

If getEligibleItemCount() hits 0 while only a microtask is queued (#isScheduledScan true, #isScanning false), you can proactively flip #isScheduledScan=false here to avoid a no‑op microtask.

   untrackEligibleItem(item: Removable): void {
     if (this.#forceDisable) {
       return
     }
@@
-    if (this.isScanning()) {
-      if (this.getEligibleItemCount() === 0) {
-        this.stopScanning()
-      } else {
-        this.#scheduleScan()
-      }
-    }
+    const count = this.getEligibleItemCount()
+    if (this.isScanning()) {
+      if (count === 0) this.stopScanning()
+      else this.#scheduleScan()
+    } else if (count === 0) {
+      // No timer yet, but a microtask may be queued
+      this.#isScheduledScan = false
+    }
   }

172-190: performScan(): safe iteration; minor clarity nit

Deleting from a Set during iteration is fine. If you prefer defensive clarity, snapshot the set first to avoid any future semantic surprises.

-  for (const item of this.#eligibleItems) {
+  for (const item of Array.from(this.#eligibleItems)) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c60f0eb and c514ec8.

📒 Files selected for processing (14)
  • packages/query-core/src/__tests__/gcManager.test.tsx (1 hunks)
  • packages/query-core/src/__tests__/mutationCache.test.tsx (1 hunks)
  • packages/query-core/src/__tests__/query.test.tsx (1 hunks)
  • packages/query-core/src/gcManager.ts (1 hunks)
  • packages/query-core/src/index.ts (1 hunks)
  • packages/query-core/src/mutation.ts (6 hunks)
  • packages/query-core/src/query.ts (8 hunks)
  • packages/query-core/src/queryClient.ts (7 hunks)
  • packages/query-core/src/removable.ts (2 hunks)
  • packages/react-query/src/__tests__/useMutation.test.tsx (1 hunks)
  • packages/react-query/src/__tests__/useQuery.test.tsx (2 hunks)
  • packages/react-query/src/__tests__/useSuspenseQueries.test.tsx (1 hunks)
  • packages/solid-query/src/__tests__/useMutation.test.tsx (1 hunks)
  • packages/solid-query/src/__tests__/useQuery.test.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/solid-query/src/tests/useMutation.test.tsx
  • packages/query-core/src/tests/mutationCache.test.tsx
  • packages/query-core/src/index.ts
  • packages/react-query/src/tests/useSuspenseQueries.test.tsx
  • packages/react-query/src/tests/useMutation.test.tsx
  • packages/solid-query/src/tests/useQuery.test.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
packages/query-core/src/__tests__/gcManager.test.tsx (2)
packages/query-core/src/queryClient.ts (1)
  • QueryClient (63-658)
packages/query-core/src/__tests__/utils.ts (1)
  • executeMutation (13-22)
packages/query-core/src/queryClient.ts (3)
packages/query-core/src/gcManager.ts (1)
  • GCManager (38-196)
packages/query-core/src/index.ts (2)
  • GCManager (17-17)
  • isServer (31-31)
packages/query-core/src/utils.ts (1)
  • isServer (78-78)
packages/react-query/src/__tests__/useQuery.test.tsx (1)
packages/query-core/src/gcManager.ts (1)
  • item (172-190)
packages/query-core/src/removable.ts (3)
packages/query-core/src/utils.ts (1)
  • isValidTimeout (93-95)
packages/query-core/src/gcManager.ts (1)
  • GCManager (38-196)
packages/query-core/src/index.ts (1)
  • GCManager (17-17)
packages/query-core/src/gcManager.ts (1)
packages/query-core/src/index.ts (3)
  • GCManager (17-17)
  • ManagedTimerId (25-25)
  • timeoutManager (24-24)
packages/query-core/src/mutation.ts (2)
packages/query-core/src/gcManager.ts (1)
  • GCManager (38-196)
packages/query-core/src/mutationObserver.ts (1)
  • MutationObserver (23-211)
packages/query-core/src/query.ts (2)
packages/query-core/src/gcManager.ts (1)
  • GCManager (38-196)
packages/query-core/src/types.ts (1)
  • QueryOptions (225-278)
🔇 Additional comments (18)
packages/query-core/src/__tests__/query.test.tsx (2)

4024-4028: Infinity gcTime should not set gc mark

Assertion that gcMarkedAt remains null after unmount when gcTime is Infinity is correct and safeguards against accidental tracking.


4032-4063: Timestamp-based GC verification looks solid

Setting system time, verifying gcMarkedAt at unmount, then advancing by gcTime to assert removal aligns with the new GC model. No changes needed.

packages/react-query/src/__tests__/useQuery.test.tsx (1)

4024-4063: React tests align with new GC semantics

The added checks for gcMarkedAt (Infinity vs finite) and the timestamp-based removal window are correct and stable under fake timers.

packages/query-core/src/removable.ts (1)

104-110: “Longest gcTime wins” defaulting is correct

Defaults (Infinity on server, 5 minutes on client) and max-merge semantics are spot on.

packages/query-core/src/query.ts (7)

207-209: Protected getGcManager accessor looks good

Provides the hook Removable needs without widening surface area.


231-239: optionalRemove(): correct boolean contract and unmarking

  • Removes when safe, returns boolean.
  • Else clears GC mark to untrack in manager.
    LGTM.

241-243: isSafeToRemove(): criteria make sense

No observers and idle fetchStatus matches previous semantics.


366-366: addObserver: clearGcMark() is correct

Prevents premature collection while observed.


387-389: removeObserver: gate GC mark on safety

Only mark when safe to remove; avoids useless scans during in‑flight fetches.


622-626: Fetch finally: re-mark for GC when safe

Ensures unused queries after settle are eligible without extra timers.


184-191: Verified: GC marking semantics are correct

Both concerns have been verified:

  • Query is added to QueryCache (via queryCache.build()this.add(query)) before the constructor completes and calls markForGc(), preventing any early collection race.
  • markForGc() is properly a no-op for gcTime=Infinity: isValidTimeout() explicitly excludes Infinity, causing markForGc() to call clearGcMark() instead of tracking the item with the GCManager.
packages/query-core/src/mutation.ts (5)

113-114: Ctor-level GC mark: verify Infinity handling

Same note as Query: ensure markForGc skips tracking for gcTime=Infinity.

Use the same script shared in Query comment to inspect Removable.


128-131: Protected getGcManager accessor looks good

Keeps GC integration encapsulated.


137-138: addObserver: clearGcMark() is correct

Prevents GC while the mutation is observed.


161-164: isSafeToRemove(): pending check is essential

Ensures we don’t collect in‑flight mutations.


165-176: optionalRemove(): smart deferral for pending, removes otherwise

Re‑marking when pending pushes gcAt forward and avoids tight 0ms rescan loops; removing when idle returns true for manager cleanup.

packages/query-core/src/gcManager.ts (2)

96-111: stopScanning(): state reset is correct

Clears timer and cancels any pending scan via flags.


198-204: getTimeUntilGc(): simple and correct

Infinity handling defers scheduling. Ensure Removable.getGcAtTimestamp() returns null for unmarked or Infinity gcTime.

Use the earlier script to validate Removable’s return contract.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/query-core/src/__tests__/gcManager.test.tsx (1)

206-207: Consider adding final state assertions for completeness.

The test correctly validates that resubscribing untracts the query (line 202). For completeness and explicit test self-containment, consider adding final assertions after the last unsubscribe:

 unsubscribe2()
+await vi.advanceTimersByTimeAsync(0)
+expect(gcManager.isScanning()).toBe(true)
+expect(gcManager.getEligibleItemCount()).toBe(1)

This would make the test's final state explicit rather than relying solely on afterEach cleanup.

packages/solid-query/src/__tests__/useQuery.test.tsx (2)

3907-3907: Tighten the Infinity case: explicit existence assert; drop fake timers (optional)

  • Add an explicit assertion for the query object to avoid masking failures behind non-null assertion.
  • Fake timers aren’t needed here (no time-based behavior exercised). Optional removal keeps the test simpler.
-    vi.useFakeTimers()
+    // No timers needed for this test

@@
-    const item = queryClient.getQueryCache().find({ queryKey: key })
-    expect(item!.gcMarkedAt).toBeNull()
+    const item = queryClient.getQueryCache().find({ queryKey: key })
+    expect(item).toBeDefined()
+    expect(item!.gcMarkedAt).toBeNull()

Also applies to: 3929-3931


3934-3937: Make eviction timing resilient to GC scan interval

The GC runs on a periodic sweep. Advance a bit beyond gcTime to avoid edge flakiness if scanInterval changes.

-    await vi.advanceTimersByTimeAsync(gcTime)
+    // Advance slightly beyond gcTime to ensure at least one sweep runs
+    await vi.advanceTimersByTimeAsync(gcTime + 50)

Also applies to: 3943-3944, 3956-3971

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c514ec8 and d74d9de.

📒 Files selected for processing (3)
  • packages/query-core/src/__tests__/gcManager.test.tsx (1 hunks)
  • packages/query-core/src/__tests__/query.test.tsx (1 hunks)
  • packages/solid-query/src/__tests__/useQuery.test.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/query-core/src/tests/query.test.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
packages/query-core/src/__tests__/gcManager.test.tsx (2)
packages/query-core/src/queryClient.ts (1)
  • QueryClient (63-658)
packages/query-core/src/__tests__/utils.ts (1)
  • executeMutation (13-22)
packages/solid-query/src/__tests__/useQuery.test.tsx (2)
packages/query-core/src/gcManager.ts (1)
  • item (172-190)
packages/query-core/src/queryObserver.ts (1)
  • query (704-721)
🔇 Additional comments (5)
packages/query-core/src/__tests__/gcManager.test.tsx (4)

1-18: LGTM: Test setup is well-structured.

The setup and teardown properly initialize fake timers, mount the QueryClient, and ensure test isolation through cleanup.


42-43: Past review issue resolved.

The debug console.log that was flagged in the previous review has been successfully removed from this section.


231-265: Excellent test that addresses the continuous scanning concern.

This test validates the fix for reviewer timdorr's concern about the GC running continuously (every 10ms) for the life of the application. It confirms that:

  • GC doesn't auto-start when idle
  • GC only starts when queries are marked for collection
  • GC stops after collecting all items
  • GC remains stopped during subsequent idle periods

This behavior aligns with the author's proposed solution to track items in a map and start the timer only when map.size > 0, preventing interference with browser backgrounding behaviors.


267-293: LGTM: Comprehensive test coverage for GCManager.

The mutation test correctly validates that mutations are tracked by the GC manager. The entire test suite provides excellent coverage of:

  • Initial idle state
  • Marking and tracking for GC
  • Collection timing and scanning lifecycle
  • Multiple items with different gcTime values
  • Reactivation/untracking
  • Edge cases (infinite gcTime, idle behavior)
  • Both queries and mutations

The tests properly use fake timers and validate the new timestamp-based GC approach.

packages/solid-query/src/__tests__/useQuery.test.tsx (1)

41-43: Good call resetting timers after each test

Prevents fake-timer leakage and cross-test flakiness.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/query-core/src/__tests__/gcManager.test.tsx (1)

1-1341: LGTM: Excellent test suite structure and coverage.

The test suite is well-organized with clear describe blocks, comprehensive coverage of initialization, basic behavior, edge cases, error handling, and integration scenarios. The progression from simple to complex tests is logical and thorough.

Optional: Consider extracting magic numbers to constants.

Throughout the tests, gcTime values like 10, 20, 50, 100 are hardcoded. While this is acceptable, extracting them to named constants (e.g., SHORT_GC_TIME = 10, MEDIUM_GC_TIME = 50) could improve readability and maintainability, especially if these values need to be adjusted.

Example:

const SHORT_GC_TIME = 10
const MEDIUM_GC_TIME = 50
const LONG_GC_TIME = 100

const observer = new QueryObserver(queryClient, {
  queryKey: key,
  queryFn: () => 'data',
  gcTime: SHORT_GC_TIME,
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d74d9de and ce1c896.

📒 Files selected for processing (1)
  • packages/query-core/src/__tests__/gcManager.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/__tests__/gcManager.test.tsx (2)
packages/query-core/src/queryClient.ts (1)
  • QueryClient (63-658)
packages/query-core/src/__tests__/utils.ts (1)
  • executeMutation (13-22)
🔇 Additional comments (10)
packages/query-core/src/__tests__/gcManager.test.tsx (10)

1-18: LGTM: Clean test setup with appropriate timer management.

The setup correctly uses fake timers for deterministic testing of time-based GC behavior, and properly cleans up in afterEach.


563-597: Excellent test that addresses the continuous scanning concern.

This test validates the solution to reviewer timdorr's concern about GC running continuously. It confirms that:

  • GC doesn't start automatically when idle
  • GC only starts when items are marked for collection
  • GC stops after collecting all items
  • GC remains stopped until new items are marked

This proves the implementation won't prevent browser backgrounding behaviors.


804-837: LGTM: Appropriate testing of environment-dependent logging behavior.

The test correctly saves and restores process.env.NODE_ENV. While environment manipulation can be fragile, this is a standard pattern for testing environment-dependent behavior, and the restoration is properly handled.


361-376: LGTM: Appropriate use of mocks for unit testing internal behavior.

These tests use mocks to test GCManager's internal tracking and timer management in isolation. This is a good practice for unit testing, and the test suite appropriately complements these with integration tests using real Query and Mutation objects.

Also applies to: 443-481


704-802: LGTM: Comprehensive error handling coverage.

These tests validate that the GC system is resilient to failures in individual items, ensuring that one failing item doesn't prevent collection of others. The tests properly mock and restore console.error, and verify that errors are logged in development but don't propagate.


1176-1340: LGTM: Excellent integration test coverage.

These integration tests cover real-world scenarios including rapid subscribe/unsubscribe cycles, many items, client remounts, and dynamic state changes during scanning. This coverage is essential for validating the GCManager's behavior in production-like conditions.


954-1067: LGTM: Comprehensive mutations integration.

These tests validate that the GCManager correctly handles mutations alongside queries, confirming the unified garbage collection approach works for all Removable types.


484-701: LGTM: Thorough edge case coverage.

The edge case tests comprehensively validate behavior across the full range of gcTime values, including Infinity, zero, very large, very small, and mixed scenarios. This ensures the GCManager handles all valid configurations correctly.


1069-1174: LGTM: Comprehensive scheduling behavior validation.

These tests validate the internal scheduling state machine, including prevention of duplicate scheduling, proper rescheduling with timeout cancellation, and correct handling of race conditions. This ensures the GCManager's scheduling logic is robust.


304-482: LGTM: Comprehensive tracking lifecycle validation.

These tests thoroughly validate the tracking and untracking lifecycle, including reactivation, duplicate prevention, safe handling of non-existent items, and proper timer cleanup. This ensures the GCManager correctly manages item tracking state.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
packages/query-core/src/__tests__/gcManager.test.tsx (6)

41-49: Baseline system time for determinism with fake timers

Set a fixed system time in beforeEach to make Date.now()-based eligibility checks fully deterministic across runs.

   beforeEach(() => {
     vi.useFakeTimers()
+    // Ensure deterministic Date.now() across tests
+    vi.setSystemTime(new Date(0))
     gcManager = new GCManager()
   })
 
   afterEach(() => {
     gcManager.clear()
     vi.useRealTimers()
   })

58-66: Add coverage for forceDisable configuration

GCManager supports forceDisable; track/untrack should no-op and never scan when enabled. Add a test here.

Example:

test('should not track or scan when forceDisable=true', async () => {
  const disabled = new GCManager({ forceDisable: true })
  const item = createMockRemovable({ gcTime: 10 })
  disabled.trackEligibleItem(item)
  await vi.advanceTimersByTimeAsync(0)
  expect(disabled.getEligibleItemCount()).toBe(0)
  expect(disabled.isScanning()).toBe(false)
  disabled.clear()
})

324-338: Clarify Infinity handling comment

The current comment contradicts itself. Keep it concise and accurate.

-      // Item with infinite gcTime should not be tracked (returns Infinity from getGcAtTimestamp)
-      // But actually, it will be tracked, just won't schedule a scan
+      // Items with Infinity gcTime remain tracked, but no scan is scheduled

510-535: Protect NODE_ENV mutation with try/finally

Ensure environment is restored even if the test fails midway.

-    const originalEnv = process.env.NODE_ENV
-    process.env.NODE_ENV = 'production'
+    const prevEnv = process.env.NODE_ENV
+    try {
+      process.env.NODE_ENV = 'production'
 
       const item = createMockRemovable({ gcTime: 10 })
 
       // Mock item to throw
       item.isEligibleForGc = vi.fn(() => {
         throw new Error('Test error')
       })
 
       gcManager.trackEligibleItem(item)
       await vi.advanceTimersByTimeAsync(0)
 
       const consoleErrorSpy = vi
         .spyOn(console, 'error')
         .mockImplementation(() => {})
 
       await vi.advanceTimersByTimeAsync(15)
 
       // Should not log in production
       expect(consoleErrorSpy).not.toHaveBeenCalled()
 
       consoleErrorSpy.mockRestore()
-      process.env.NODE_ENV = originalEnv
+    } finally {
+      process.env.NODE_ENV = prevEnv
+    }

1-36: Optional: microtask vs timers flushing helper

Many tests use await vi.advanceTimersByTimeAsync(0) to flush the queueMicrotask scheduling. To make intent explicit and avoid inadvertently firing zero-delay timers where not needed, add and use a tiny helper for microtasks only.

Example helper at top-level:

const flushMicrotasks = () => Promise.resolve()

Then replace occurrences where only the microtask needs flushing (not a 0ms timeout) with:

await flushMicrotasks()

759-787: Optional: cover “untrack before schedule” path

Add a test where you track an item and immediately untrack it before flushing the microtask; assert that no timeout is scheduled and nothing runs after advancing time.

Example:

test('should no-op if item is untracked before schedule completes', async () => {
  const item = createMockRemovable({ gcTime: 50, isEligibleFn: () => true })
  gcManager.trackEligibleItem(item)
  // Untrack before microtask runs
  gcManager.untrackEligibleItem(item)
  await vi.advanceTimersByTimeAsync(0)
  await vi.advanceTimersByTimeAsync(100)
  expect(gcManager.isScanning()).toBe(false)
  expect(gcManager.getEligibleItemCount()).toBe(0)
  expect(item.isEligibleForGc).not.toHaveBeenCalled()
  expect(item.optionalRemove).not.toHaveBeenCalled()
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce1c896 and b030a69.

📒 Files selected for processing (1)
  • packages/query-core/src/__tests__/gcManager.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/query-core/src/__tests__/gcManager.test.tsx (1)
packages/query-core/src/gcManager.ts (2)
  • GCManager (38-196)
  • item (172-190)
🔇 Additional comments (4)
packages/query-core/src/__tests__/gcManager.test.tsx (4)

69-79: LGTM: scanning starts only when needed

Starts scanning after the first eligible item and asserts state correctly. This aligns with GCManager’s on-demand scheduling (no idle churn).


289-320: Great: verifies timeout cleanup on untrack

Solid assertion that no scan fires after untracking (isEligibleForGc/optionalRemove never called).


372-398: Addresses idle-tab concern: no continuous scanning when idle

This test explicitly ensures scanning doesn’t run in the absence of eligible items, addressing reviewer feedback about background tab behavior.

If you have an integration test harness for the browser environment, consider a follow-up test that simulates page backgrounding (document.hidden) to ensure no unexpected timers leak.


661-679: Nice: stopScanning before microtask completes

Covers the race where stopScanning is called prior to scheduled timeout creation; behavior matches GCManager’s #isScheduledScan guard.

@MrFlashAccount
Copy link
Author

Closing to recreate this PR, as the implementation has been significantly changed.
New implementation uses timeouts instead of intervals and schedule it only if we have items to GC.

@MrFlashAccount MrFlashAccount deleted the improve-gc-performance branch October 29, 2025 14:54
@MrFlashAccount MrFlashAccount restored the improve-gc-performance branch October 29, 2025 14:54
@MrFlashAccount
Copy link
Author

New PR: #9827

CC @timdorr

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants