Skip to content

perf(queriesObserver): fix O(n²) performance issue in batch updates - #9467

Closed
joseph0926 wants to merge 1 commit into
TanStack:mainfrom
joseph0926:perf/queries-observer-batch-updates
Closed

perf(queriesObserver): fix O(n²) performance issue in batch updates#9467
joseph0926 wants to merge 1 commit into
TanStack:mainfrom
joseph0926:perf/queries-observer-batch-updates

Conversation

@joseph0926

@joseph0926joseph0926 commented Jul 20, 2025

Copy link
Copy Markdown
Contributor

This is an additional workaround attempt for an issue I've been challenged to solve before.
Previous issue
First Resolution Attempt PR

Note: I'm not a native English speaker, so I've used AI to help organize and articulate my thoughts clearly in this PR.

Background

This PR addresses a long-standing performance issue first reported in #8295, where using useQueries with batch data fetching patterns (like DataLoader) causes severe performance degradation due to O(n²) complexity.

The Problem

When multiple queries resolve simultaneously (common with DataLoader pattern), the performance degrades quadratically:

// Example: DataLoader batches multiple requestsconstuserLoader=newDataLoader(async(ids)=>{constusers=awaitfetch(`/api/users?ids=${ids.join(',')}`);returnusers;// Returns [user1, user2, user3, ...] all at once});// Used with useQueriesconstqueries=useQueries({queries: userIds.map(id=>({queryKey: ["user",id],queryFn: ()=>userLoader.load(id),})),});

The issue occurs because:

  1. DataLoader fetches all data in one batch
  2. Each individual promise resolves separately
  3. Each resolution triggers an update that searches through ALL observers
  4. Result: n queries × n searches = O(n²) complexity

extra notes

i found two remaining pain‑points:

  1. Full‑array scans even when the target observer is already known
// Previous implementation — O(N²)functiondifference<T>(a: T[],b: T[]): T[]{returna.filter((x)=>!b.includes(x))// includes ⇒ O(N) per element}

If 100 observers change only 1 member, we still perform
100 × 100 = 10 000comparisons.

  1. Missing early‑return / direct access

    // onUpdate still does a linear search every timeconstindex=this.#observers.indexOf(observer)// O(N) per update
    // trackResult recomputes matches on every callconstmatches=this.#findMatchingObservers(queries)

Concrete mental model

Imagine a courier delivering 100 packages to 100 buildings:
– For each package he checks every single building (101 → 200)
– He repeats that 100 times.
Total look‑ups: 100 packages × 100 buildings = 10 000

Real-world impact

  • 100 queries = 10,000 operations (noticeable lag)
  • 1,000 queries = 1,000,000 operations (browser freeze)

Previous Improvements

Over the past months, several optimizations have been made:

1. Caching findMatchingObservers results (#8304)

I previously added observerMatches to cache results instead of recalculating:

// Before: Called findMatchingObservers repeatedly// After: Cache and reusethis.#observerMatches =newObserverMatches

2. Optimizing difference function (O(n²) → O(n))

// Before: O(n²) with includesfunctiondifference<T>(array1: Array<T>,array2: Array<T>): Array<T>{returnarray1.filter((x)=>!array2.includes(x))}// After: O(n) with Setfunctiondifference<T>(array1: Array<T>,array2: Array<T>): Array<T>{constexcludeSet=newSet(array2)returnarray1.filter((x)=>!excludeSet.has(x))}

3. Optimizing findMatchingObservers with Map

// Now uses Map for O(1) lookupsconstprevObserversMap=newMap(this.#observers.map((observer)=>[observer.options.queryHash,observer]),)

The Remaining Issue

Despite these improvements, one critical O(n) operation remains in #onUpdate:

#onUpdate(observer: QueryObserver,result: QueryObserverResult): void{constindex=this.#observers.indexOf(observer)// O(n) search!if(index!==-1){this.#result =replaceAt(this.#result,index,result)this.#notify()}}

With batch updates, this creates:

  • 100 updates × 100 searches = 10,000 operations
  • 1,000 updates × 1,000 searches = 1,000,000 operations

This PR's Solution

This PR introduces a WeakMap to track observer indices, eliminating the O(n) search:

exportclassQueriesObserver{
#indexMap: WeakMap<QueryObserver,number>=newWeakMap()setQueries(...){// Update index map whenever observers changethis.#indexMap =newWeakMap()newObservers.forEach((observer,index)=>{this.#indexMap.set(observer,index)})}
#onUpdate(observer: QueryObserver,result: QueryObserverResult): void{// O(1) lookup instead of O(n) searchconstindex=this.#indexMap.get(observer)if(index!==undefined){this.#result =replaceAt(this.#result,index,result)this.#notify()}}}

Why WeakMap?

I chose WeakMap over regular Map to address the concern raised in #8686 about storing observers in both a Map and an Array:

"I'm not a fan of storing the observers in both a Map and an Array, and the whole thing becomes more complex."

WeakMap solves this elegantly:

  • Not dual storage: WeakMap only stores indices, not observers themselves. The Array remains the single source of truth for observers
  • Automatic cleanup: When observers are removed from the array, WeakMap entries are automatically garbage collected - no manual synchronization needed
  • Minimal overhead: Acts purely as a lookup table without adding complexity to the data model
  • Memory efficient: No risk of memory leaks from orphaned references

This approach maintains the simplicity of the original design while achieving O(1) performance.

Performance Results

With this change, batch updates now scale linearly:

  • Before: O(n²) - 100 queries took ~10,000 operations
  • After: O(n) - 100 queries take ~100 operations
  • Improvement: ~100x faster for typical DataLoader use cases

Summary

This completes the optimization journey for QueriesObserver:

  1. findMatchingObservers caching (my previous PR)
  2. difference function: O(n²) → O(n)
  3. findMatchingObservers: O(n) → O(1) with Map
  4. #onUpdate: O(n) → O(1) with WeakMap (this PR)

The combination of these improvements transforms what was once a quadratic bottleneck into efficient linear scaling, making useQueries viable for large-scale batch operations.

@nx-cloud

nx-cloudBot commented Jul 20, 2025

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5f51b4a

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded3m 48sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded1m 19sView ↗

☁️ Nx Cloud last updated this comment at 2025-07-20 02:58:15 UTC

@pkg-pr-new

Copy link
Copy Markdown
More templates

@tanstack/angular-query-devtools-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-devtools-experimental@9467

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9467

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9467

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9467

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9467

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@9467

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@9467

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9467

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9467

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@9467

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@9467

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9467

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9467

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@9467

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9467

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9467

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@9467

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9467

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9467

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@9467

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9467

commit: 5f51b4a

@codecov

codecovBot commented Jul 20, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.60%. Comparing base (05c62a0) to head (5f51b4a).
⚠️ Report is 37 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@ Coverage Diff @@## main #9467 +/- ##
===========================================
+ Coverage 45.30% 59.60% +14.29% 
===========================================
Files 208 137 -71 Lines 8283 5525 -2758 Branches 1869 1477 -392 ===========================================
- Hits 3753 3293 -460 + Misses 4085 1930 -2155 + Partials 445 302 -143 
ComponentsCoverage Δ
@tanstack/angular-query-devtools-experimental∅ <ø> (∅)
@tanstack/angular-query-experimental85.00% <ø> (ø)
@tanstack/eslint-plugin-query∅ <ø> (∅)
@tanstack/query-async-storage-persister43.85% <ø> (ø)
@tanstack/query-broadcast-client-experimental24.39% <ø> (ø)
@tanstack/query-codemods∅ <ø> (∅)
@tanstack/query-core97.69% <100.00%> (+<0.01%)⬆️
@tanstack/query-devtools3.55% <ø> (ø)
@tanstack/query-persist-client-core79.47% <ø> (ø)
@tanstack/query-sync-storage-persister84.61% <ø> (ø)
@tanstack/query-test-utils∅ <ø> (∅)
@tanstack/react-query95.95% <ø> (ø)
@tanstack/react-query-devtools10.00% <ø> (ø)
@tanstack/react-query-next-experimental∅ <ø> (∅)
@tanstack/react-query-persist-client100.00% <ø> (ø)
@tanstack/solid-query78.13% <ø> (ø)
@tanstack/solid-query-devtools∅ <ø> (∅)
@tanstack/solid-query-persist-client100.00% <ø> (ø)
@tanstack/svelte-query87.58% <ø> (ø)
@tanstack/svelte-query-devtools∅ <ø> (∅)
@tanstack/svelte-query-persist-client100.00% <ø> (ø)
@tanstack/vue-query71.10% <ø> (ø)
@tanstack/vue-query-devtools∅ <ø> (∅)
🚀 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 on lines -255 to +262
const index = this.#observers.indexOf(observer)
if (index !== -1) {
const index = this.#indexMap.get(observer)
if (index !== undefined) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why don’t we just pass the index into onUpdate instead of passing the whole observer and then trying to find the index again? For example:

diff --git a/packages/query-core/src/queriesObserver.ts b/packages/query-core/src/queriesObserver.ts
index 853e490ab..84defa874 100644
--- a/packages/query-core/src/queriesObserver.ts+++ b/packages/query-core/src/queriesObserver.ts@@ -63,9 +63,9 @@ export class QueriesObserver<
protected onSubscribe(): void {
if (this.listeners.size === 1) {
- this.#observers.forEach((observer) => {+ this.#observers.forEach((observer, index) => {
observer.subscribe((result) => {
- this.#onUpdate(observer, result)+ this.#onUpdate(index, result)
})
})
}
@@ -137,9 +137,9 @@ export class QueriesObserver<
observer.destroy()
})
- difference(newObservers, prevObservers).forEach((observer) => {+ difference(newObservers, prevObservers).forEach((observer, index) => {
observer.subscribe((result) => {
- this.#onUpdate(observer, result)+ this.#onUpdate(index, result)
})
})
@@ -251,12 +251,9 @@ export class QueriesObserver<
return observers
}
- #onUpdate(observer: QueryObserver, result: QueryObserverResult): void {- const index = this.#observers.indexOf(observer)- if (index !== -1) {- this.#result = replaceAt(this.#result, index, result)- this.#notify()- }+ #onUpdate(index: number, result: QueryObserverResult): void {+ this.#result = replaceAt(this.#result, index, result)+ this.#notify()
}
#notify(): void {

@joseph0926joseph0926Jul 31, 2025

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the suggestion
I considered passing the index directly, but noticed these potential issues

When setQueries() reorders observers, existing subscriptions keep their old captured indices

// Initial: observers = [A, B, C]// A subscribes with index=0 in onSubscribe()// After setQueries(): observers = [D, A, B, C] // A still uses index=0, but should use index=1

The index from difference().forEach() is not the actual position in this.#observers

difference(newObservers,prevObservers).forEach((observer,index)=>{// If difference returns [D, E], index is 0,1// But their actual positions in observers might be [A, D, B, E, C] -> 1,3observer.subscribe((result)=>{this.#onUpdate(index,result)// <- I think there is a problem here.})})

What do you think about these concerns?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Right, can you then please add a test case where the implementation I suggested would fail.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

import{afterEach,beforeEach,describe,expect,test,vi}from'vitest'import{queryKey}from'@tanstack/query-test-utils'import{QueriesObserver,QueryClient}from'..'importtype{QueryObserverResult}from'..'describe('queriesObserver - index tracking issue',()=>{letqueryClient: QueryClientbeforeEach(()=>{vi.useFakeTimers()queryClient=newQueryClient()queryClient.mount()})afterEach(()=>{queryClient.clear()vi.useRealTimers()})test('should fail when using forEach index with dynamic query changes',async()=>{constkey1=queryKey()constkey2=queryKey()constkey3=queryKey()constkey4=queryKey()constqueryFn1=vi.fn().mockReturnValue('data1')constqueryFn2=vi.fn().mockReturnValue('data2')constqueryFn3=vi.fn().mockReturnValue('data3')constqueryFn4=vi.fn().mockReturnValue('data4')constobserver=newQueriesObserver(queryClient,[{queryKey: key1,queryFn: queryFn1},{queryKey: key2,queryFn: queryFn2},])constresults: Array<Array<QueryObserverResult>>=[]constunsubscribe=observer.subscribe((result)=>{results.push([...result])})awaitvi.advanceTimersByTimeAsync(0)results.length=0observer.setQueries([{queryKey: key3,queryFn: queryFn3},{queryKey: key1,queryFn: queryFn1},{queryKey: key4,queryFn: queryFn4},{queryKey: key2,queryFn: queryFn2},])awaitvi.advanceTimersByTimeAsync(0)queryClient.setQueryData(key3,'updated3')queryClient.setQueryData(key4,'updated4')queryClient.setQueryData(key1,'updated1')unsubscribe()constfinalResult=results[results.length-1]expect(finalResult).toHaveLength(4)expect(finalResult?.[0]).toMatchObject({data: 'updated3'})expect(finalResult?.[1]).toMatchObject({data: 'updated1'})expect(finalResult?.[2]).toMatchObject({data: 'updated4'})expect(finalResult?.[3]).toMatchObject({data: 'data2'})})test('should fail when reordering queries with existing subscriptions',async()=>{constkey1=queryKey()constkey2=queryKey()constkey3=queryKey()letupdateCount=0constqueryFn1=vi.fn().mockImplementation(()=>`data1-${++updateCount}`)constqueryFn2=vi.fn().mockImplementation(()=>`data2-${++updateCount}`)constqueryFn3=vi.fn().mockImplementation(()=>`data3-${++updateCount}`)constobserver=newQueriesObserver(queryClient,[{queryKey: key1,queryFn: queryFn1},{queryKey: key2,queryFn: queryFn2},{queryKey: key3,queryFn: queryFn3},])constresults: Array<Array<QueryObserverResult>>=[]constunsubscribe=observer.subscribe((result)=>{results.push([...result])})awaitvi.advanceTimersByTimeAsync(0)results.length=0observer.setQueries([{queryKey: key3,queryFn: queryFn3},{queryKey: key1,queryFn: queryFn1},{queryKey: key2,queryFn: queryFn2},])awaitqueryClient.invalidateQueries({queryKey: key1})awaitvi.advanceTimersByTimeAsync(0)constresultAfterInvalidate=results[results.length-1]expect(resultAfterInvalidate?.[1]?.data).toMatch(/data1-\d+/)expect(resultAfterInvalidate?.[0]?.data).toBe('data3-3')expect(resultAfterInvalidate?.[2]?.data).toBe('data2-2')unsubscribe()})})
queries-test

the failure only occurs with the version that captures the index in the subscription callback. When I run the same tests against either the current main branch’s queriesObserver.ts or the queriesObserver.ts in my PR, all of the tests pass without any issues.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I believe you, but then please add the test case to the PR.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Following your suggestion, I've created and run various performance tests, and I'd like to share the results. While theoretically replacing indexOf O(n) with WeakMap O(1) should improve performance, the actual test results were different from what I expected.

  • Even in extreme cases with 10,000 queries, there was no meaningful performance difference
  • In some cases, the WeakMap version was actually slightly slower

It seems that other optimizations like using Set in the difference function and caching in findMatchingObservers have already resolved the main O(n²) issues.
I apologize for taking up your valuable time reviewing a PR that ultimately doesn't provide substantial improvements. I would appreciate it if you could close the PR.
This has been a great learning experience, and I'm grateful for your time and feedback. Thank you for the opportunity to contribute to the project.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

For reference, I am sharing one of the codes I tested.

import{afterEach,beforeEach,describe,expect,test}from'vitest'import{QueriesObserver,QueryClient}from'..'importtype{QueryObserverOptions}from'..'describe('QueriesObserver extreme cases - Real-time Dashboard',()=>{letqueryClient: QueryClientbeforeEach(()=>{queryClient=newQueryClient({defaultOptions: {queries: {retry: false,},},})queryClient.mount()})afterEach(()=>{queryClient.clear()})test('should handle real-time dashboard with 10,000+ queries where last ones update frequently',async()=>{constTOTAL_QUERIES=10000constACTIVE_QUERIES=100constUPDATE_ROUNDS=10console.log(`\n Real-time Dashboard Simulation:`)console.log(`Total queries: ${TOTAL_QUERIES}`)console.log(`Active queries (frequently updating): ${ACTIVE_QUERIES}`)console.log(`Update rounds: ${UPDATE_ROUNDS}\n`)constcreateQueries=(round: number=0): Array<QueryObserverOptions>=>{returnArray.from({length: TOTAL_QUERIES},(_,i)=>{constisActive=i>=TOTAL_QUERIES-ACTIVE_QUERIESreturn{queryKey: ['dashboard',i,round],queryFn: async()=>{awaitnewPromise((resolve)=>setTimeout(resolve,0))return{id: i,data: `${round===0 ? 'initial' : `update-${round}`}-${i}`,timestamp: Date.now(),
isActive,}},staleTime: isActive ? 0 : Infinity,}})}conststartSetup=performance.now()constobserver=newQueriesObserver(queryClient,createQueries(0))lettotalUpdates=0constupdateTimes: Array<number>=[]letlastUpdateTime=performance.now()constupdateMetrics: Array<{round: numberupdateCount: numberduration: numberavgTimePerUpdate: number}>=[]constunsubscribe=observer.subscribe(()=>{constnow=performance.now()updateTimes.push(now-lastUpdateTime)lastUpdateTime=nowtotalUpdates++})constsetupTime=performance.now()-startSetupconsole.log(`Setup time: ${setupTime.toFixed(1)}ms`)console.log('\n Initial load...')constinitialLoadStart=performance.now()awaitnewPromise((resolve)=>setTimeout(resolve,100))constinitialLoadTime=performance.now()-initialLoadStartconstinitialUpdateCount=totalUpdatesconsole.log(`Initial load completed: ${initialLoadTime.toFixed(1)}ms, ${initialUpdateCount} updates`,)console.log('\n Starting frequent updates on active queries...')for(letround=1;round<=UPDATE_ROUNDS;round++){constroundStart=performance.now()constupdateCountBefore=totalUpdatesconstupdatedQueries=Array.from({length: TOTAL_QUERIES},(_,i)=>{constisActive=i>=TOTAL_QUERIES-ACTIVE_QUERIESif(isActive){return{queryKey: ['dashboard',i,round],queryFn: async()=>{awaitnewPromise((resolve)=>setTimeout(resolve,0))return{id: i,data: `update-${round}-${i}`,timestamp: Date.now(),isActive: true,}},staleTime: 0,}}else{return{queryKey: ['dashboard',i,0],queryFn: async()=>{return{id: i,data: `initial-${i}`,timestamp: Date.now(),isActive: false,}},staleTime: Infinity,}}})observer.setQueries(updatedQueries)awaitnewPromise((resolve)=>setTimeout(resolve,50))constroundDuration=performance.now()-roundStartconstroundUpdates=totalUpdates-updateCountBeforeupdateMetrics.push({
round,updateCount: roundUpdates,duration: roundDuration,avgTimePerUpdate: roundDuration/ACTIVE_QUERIES,})}console.log('\n Performance Analysis:')console.log('Round | Duration | Updates | Avg/Update')console.log('------|----------|---------|------------')updateMetrics.forEach(({ round, duration, updateCount, avgTimePerUpdate })=>{console.log(`${round.toString().padEnd(5)} | ${duration.toFixed(1).padEnd(8)}ms | ${updateCount.toString().padEnd(7)} | ${avgTimePerUpdate.toFixed(2)}ms`,)},)constvalidMetrics=updateMetrics.filter((m)=>m.updateCount>0)if(validMetrics.length>=2){constfirstRoundAvg=validMetrics[0].avgTimePerUpdateconstlastRoundAvg=validMetrics[validMetrics.length-1].avgTimePerUpdateconstdegradation=lastRoundAvg/firstRoundAvgconsole.log(`\n Performance degradation: ${degradation.toFixed(2)}x`)console.log(`Total updates processed: ${totalUpdates}`)if(updateTimes.length>100){constsortedTimes=[...updateTimes].sort((a,b)=>a-b)constp50=sortedTimes[Math.floor(sortedTimes.length*0.5)]constp95=sortedTimes[Math.floor(sortedTimes.length*0.95)]constp99=sortedTimes[Math.floor(sortedTimes.length*0.99)]console.log('\n Update time distribution:')console.log(`P50: ${p50?.toFixed(2)}ms`)console.log(`P95: ${p95?.toFixed(2)}ms`)console.log(`P99: ${p99?.toFixed(2)}ms`)}expect(degradation).toBeLessThan(3.0)}unsubscribe()})test('should demonstrate O(n) vs O(n²) behavior with increasing observer counts',async()=>{consttestSizes=[1000,2000,4000,8000]constresults: Array<{size: numbersetupTime: numberupdateTime: numbertimePerQuery: number}>=[]console.log('\n Scalability Analysis:')for(constsizeoftestSizes){constactiveCount=Math.min(100,size/10)constqueries=Array.from({length: size},(_,i)=>({queryKey: ['scale-test',size,i],queryFn: async()=>({id: i,data: `result-${i}`}),staleTime: i>=size-activeCount ? 0 : Infinity,}))constsetupStart=performance.now()constobserver=newQueriesObserver(queryClient,queries)letupdateCount=0constunsubscribe=observer.subscribe(()=>{updateCount++})constsetupTime=performance.now()-setupStartawaitnewPromise((resolve)=>setTimeout(resolve,50))constupdateStart=performance.now()constupdatedQueries=queries.map((q,i)=>{if(i>=size-activeCount){return{
...q,queryKey: ['scale-test',size,i,'updated'],}}returnq})observer.setQueries(updatedQueries)awaitnewPromise((resolve)=>setTimeout(resolve,50))constupdateTime=performance.now()-updateStartresults.push({
size,
setupTime,
updateTime,timePerQuery: updateTime/activeCount,})unsubscribe()awaitqueryClient.clear()}console.log('Size | Setup Time | Update Time | Time/Query | Growth')console.log('------|------------|-------------|------------|--------')results.forEach((result,i)=>{constgrowth=i>0
? (result.timePerQuery/results[0].timePerQuery).toFixed(2)
: '1.00'console.log(`${result.size.toString().padEnd(5)} | ${result.setupTime.toFixed(1).padEnd(10)}ms | ${result.updateTime.toFixed(1).padEnd(11)}ms | ${result.timePerQuery.toFixed(2).padEnd(10)}ms | ${growth}x`,)})consttimePerQueryRatios=results.map((r)=>r.timePerQuery)constmaxRatio=Math.max(...timePerQueryRatios)constminRatio=Math.min(...timePerQueryRatios)constvarianceRatio=maxRatio/minRatioconsole.log(`\nTime per query variance: ${varianceRatio.toFixed(2)}x (should be close to 1.0 for O(n))`,)expect(varianceRatio).toBeLessThan(3.0)})})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

alright, let’s close this then. Thanks for working on this 🙏

Sign up for freeto 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

@joseph0926@TkDodo