UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

342 lines (288 loc) 11.1 kB
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query' import { setupApiStore } from '../../tests/utils/helpers' // Test Configuration const NUM_ENDPOINTS = 10 const NUM_TAGS = 5 const NUM_CACHE_ENTRIES = 300 const TAG_TO_INVALIDATE = 'Tag2' // Will affect ~60 entries (20% of cache) // Tag types for the test const tagTypes = Array.from({ length: NUM_TAGS }, (_, i) => `Tag${i}`) as [ string, ...string[], ] describe('Tag Invalidation Performance', () => { test('measures CPU time for tag invalidation with large cache', async () => { // 1. Create API with multiple endpoints const api = createApi({ baseQuery: fakeBaseQuery(), tagTypes, endpoints: (build) => { const endpoints: any = {} // Create 10 endpoints, each providing different tag combinations for (let i = 0; i < NUM_ENDPOINTS; i++) { endpoints[`endpoint${i}`] = build.query({ queryFn: (id: number) => ({ data: { id, endpoint: i } }), providesTags: (result: any, error: any, id: number) => { // Each endpoint provides 2-3 tags based on ID const tags: any[] = [ `Tag${id % NUM_TAGS}`, { type: `Tag${(id + 1) % NUM_TAGS}`, id }, ] if (id % 3 === 0) { tags.push({ type: `Tag${(id + 2) % NUM_TAGS}`, id: 'LIST' }) } return tags }, }) } // Add mutation that invalidates a specific tag endpoints.invalidateTag = build.mutation({ queryFn: (tag: string) => ({ data: { tag } }), invalidatesTags: (result: any, error: any, tag: string) => [tag], }) return endpoints }, }) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) // 2. Populate cache with 300 entries using upsertQueryEntries const startSetup = performance.now() const entries = [] for (let endpointIdx = 0; endpointIdx < NUM_ENDPOINTS; endpointIdx++) { for (let id = 0; id < NUM_CACHE_ENTRIES / NUM_ENDPOINTS; id++) { entries.push({ endpointName: `endpoint${endpointIdx}`, arg: id + endpointIdx * 100, value: { id: id + endpointIdx * 100, endpoint: endpointIdx, data: `Sample data ${id}`, }, }) } } // Batch upsert all entries at once storeRef.store.dispatch(api.util.upsertQueryEntries(entries as any)) const setupTime = performance.now() - startSetup console.log( `Setup time (${NUM_CACHE_ENTRIES} entries): ${setupTime.toFixed(2)}ms`, ) // 3. Verify cache is populated const state = storeRef.store.getState() const cacheSize = Object.keys(state.api.queries).length expect(cacheSize).toBe(NUM_CACHE_ENTRIES) // 4. Count how many entries will be affected by invalidation const affectedEntries = api.util.selectInvalidatedBy(state, [ TAG_TO_INVALIDATE, ]) console.log( `Entries affected by invalidating ${TAG_TO_INVALIDATE}: ${affectedEntries.length}`, ) // 5. Measure invalidation performance const startInvalidation = performance.now() await storeRef.store.dispatch( (api.endpoints as any).invalidateTag.initiate(TAG_TO_INVALIDATE), ) const invalidationTime = performance.now() - startInvalidation console.log(`Invalidation time: ${invalidationTime.toFixed(2)}ms`) // 6. Performance assertions expect(setupTime).toBeLessThan(200) // Setup should be reasonably fast expect(invalidationTime).toBeLessThan(200) // Invalidation should be fast // 7. Verify invalidation worked const newState = storeRef.store.getState() const invalidatedCount = affectedEntries.filter(({ queryCacheKey }) => { const entry = newState.api.queries[queryCacheKey] return entry?.status === 'pending' // Should be refetching }).length console.log(`Entries refetching after invalidation: ${invalidatedCount}`) }) test('measures selectInvalidatedBy performance with various tag patterns', () => { // Similar setup but focuses on selector performance const api = createApi({ baseQuery: fakeBaseQuery(), tagTypes, endpoints: (build) => ({ // Simplified endpoint for this test getData: build.query({ queryFn: (id: number) => ({ data: { id } }), providesTags: (result: any, error: any, id: number) => [ `Tag${id % NUM_TAGS}`, { type: `Tag${(id + 1) % NUM_TAGS}` as any, id }, { type: `Tag${(id + 2) % NUM_TAGS}` as any, id: 'LIST' }, ], }), }), }) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) // Populate cache const entries = Array.from({ length: NUM_CACHE_ENTRIES }, (_, i) => ({ endpointName: 'getData', arg: i, value: { id: i, data: `Data ${i}` }, })) storeRef.store.dispatch(api.util.upsertQueryEntries(entries as any)) const state = storeRef.store.getState() // Test different invalidation patterns const patterns = [ { name: 'Single type tag', tags: ['Tag0'] }, { name: 'Multiple type tags', tags: ['Tag0', 'Tag1', 'Tag2'] }, { name: 'Specific ID tag', tags: [{ type: 'Tag0', id: 5 }] }, { name: 'LIST tag', tags: [{ type: 'Tag0', id: 'LIST' }] }, { name: 'Mixed tags', tags: ['Tag0', { type: 'Tag1', id: 10 }, { type: 'Tag2', id: 'LIST' }], }, ] patterns.forEach(({ name, tags }) => { const start = performance.now() const affected = api.util.selectInvalidatedBy(state, tags as any) const duration = performance.now() - start console.log( `${name}: ${affected.length} entries in ${duration.toFixed(3)}ms`, ) expect(duration).toBeLessThan(20) // Should be very fast }) }) test('measures hasPendingRequests performance with large cache', async () => { // This test specifically targets the hasPendingRequests optimization opportunity const api = createApi({ baseQuery: fakeBaseQuery(), tagTypes, endpoints: (build) => ({ getData: build.query({ queryFn: (id: number) => ({ data: { id } }), providesTags: ['Tag0'], }), invalidateAll: build.mutation({ queryFn: () => ({ data: {} }), invalidatesTags: ['Tag0'], }), }), }) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) // Populate with many entries const entries = Array.from({ length: NUM_CACHE_ENTRIES }, (_, i) => ({ endpointName: 'getData', arg: i, value: { id: i }, })) storeRef.store.dispatch(api.util.upsertQueryEntries(entries as any)) // Trigger invalidation which will cause hasPendingRequests to be called repeatedly const start = performance.now() await storeRef.store.dispatch( api.endpoints.invalidateAll.initiate(undefined) as any, ) const duration = performance.now() - start console.log( `Invalidation triggering hasPendingRequests: ${duration.toFixed(2)}ms`, ) // This should highlight the O(n) iteration issue if it exists expect(duration).toBeLessThan(200) }) test('measures tag calculation performance during cache population', () => { // This test measures the performance of calculateProvidedBy const api = createApi({ baseQuery: fakeBaseQuery(), tagTypes, endpoints: (build) => ({ getComplexData: build.query({ queryFn: (id: number) => ({ data: { id } }), providesTags: (result: any, error: any, id: number) => { // Complex tag calculation that returns many tags const tags: any[] = [] for (let i = 0; i < NUM_TAGS; i++) { tags.push(`Tag${i}`) tags.push({ type: `Tag${i}`, id }) if (id % 2 === 0) { tags.push({ type: `Tag${i}`, id: 'LIST' }) } } return tags }, }), }), }) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) // Measure time to populate cache with complex tag calculations const entries = Array.from({ length: 100 }, (_, i) => ({ endpointName: 'getComplexData', arg: i, value: { id: i, data: `Complex data ${i}` }, })) const start = performance.now() storeRef.store.dispatch(api.util.upsertQueryEntries(entries as any)) const duration = performance.now() - start console.log( `Tag calculation for 100 entries with ${NUM_TAGS * 3} tags each: ${duration.toFixed(2)}ms`, ) // Verify tags were calculated correctly const state = storeRef.store.getState() const providedTags = state.api.provided // Count total tag subscriptions let totalSubscriptions = 0 Object.values(providedTags.tags).forEach((tagIds: any) => { Object.values(tagIds).forEach((cacheKeys: any) => { totalSubscriptions += cacheKeys.length }) }) console.log(`Total tag subscriptions created: ${totalSubscriptions}`) expect(totalSubscriptions).toBeGreaterThan(0) expect(duration).toBeLessThan(100) }) test('measures performance of repeated invalidations', async () => { // This test simulates a scenario where multiple mutations invalidate tags rapidly const api = createApi({ baseQuery: fakeBaseQuery(), tagTypes, endpoints: (build) => ({ getData: build.query({ queryFn: (id: number) => ({ data: { id } }), providesTags: (result: any, error: any, id: number) => [ `Tag${id % NUM_TAGS}`, ], }), updateData: build.mutation({ queryFn: (id: number) => ({ data: { id } }), invalidatesTags: (result: any, error: any, id: number) => [ `Tag${id % NUM_TAGS}`, ], }), }), }) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) // Populate cache const entries = Array.from({ length: 200 }, (_, i) => ({ endpointName: 'getData', arg: i, value: { id: i }, })) storeRef.store.dispatch(api.util.upsertQueryEntries(entries as any)) // Perform multiple rapid invalidations const numInvalidations = 10 const start = performance.now() const promises = [] for (let i = 0; i < numInvalidations; i++) { promises.push( storeRef.store.dispatch( api.endpoints.updateData.initiate(i % NUM_TAGS) as any, ), ) } await Promise.all(promises) const duration = performance.now() - start const avgPerInvalidation = duration / numInvalidations console.log( `${numInvalidations} rapid invalidations: ${duration.toFixed(2)}ms total, ${avgPerInvalidation.toFixed(2)}ms avg`, ) expect(avgPerInvalidation).toBeLessThan(50) }) })