Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(graphcache): Remove refLock and add dirtyKeys / skip GC runs in unsettled states (#2862)

authored by

Phil Pluckthun and committed by
GitHub
02d6d96f ece64fcf

+59 -100
+5
.changeset/orange-squids-care.md
··· 1 + --- 2 + '@urql/exchange-graphcache': patch 3 + --- 4 + 5 + Add skipping of garbage collection runs when the cache is waiting for optimistic, deferred or other results in layers. This means that we only take an opportunity to run garbage collection after results have settled and are hence decreasing the chance of hogging the event loop when a run isn't needed.
+1 -1
exchanges/graphcache/src/store/data.test.ts
··· 87 87 InMemoryData.reserveLayer(data, 1); 88 88 InMemoryData.gc(); 89 89 90 - expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); 90 + expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); 91 91 expect(InMemoryData.getCurrentDependencies()).toEqual( 92 92 new Set(['Query.todo', 'Todo:1']) 93 93 );
+53 -99
exchanges/graphcache/src/store/data.ts
··· 41 41 queryRootKey: string; 42 42 /** Number of references to each entity (except "Query") */ 43 43 refCount: KeyMap<number>; 44 - /** Number of references to each entity on optimistic layers */ 45 - refLock: OperationMap<KeyMap<number>>; 46 44 /** A map of entity fields (key-value entries per entity) */ 47 45 records: NodeMap<EntityField>; 48 46 /** A map of entity links which are connections from one entity to another (key-value entries per entity) */ ··· 51 49 deferredKeys: Set<number>; 52 50 /** A set of Query operation keys that are in-flight and awaiting a result */ 53 51 commutativeKeys: Set<number>; 52 + /** A set of Query operation keys that have been written to */ 53 + dirtyKeys: Set<number>; 54 54 /** The order of optimistic layers */ 55 55 optimisticOrder: number[]; 56 56 /** This may be a persistence adapter that will receive changes in a batch */ ··· 160 160 let i = data.optimisticOrder.length; 161 161 while ( 162 162 --i >= 0 && 163 - data.refLock.has(data.optimisticOrder[i]) && 163 + data.dirtyKeys.has(data.optimisticOrder[i]) && 164 164 data.commutativeKeys.has(data.optimisticOrder[i]) 165 165 ) 166 166 squashLayer(data.optimisticOrder[i]); ··· 175 175 currentDebugStack.length = 0; 176 176 } 177 177 178 - // Schedule deferred tasks if we haven't already 179 - if (process.env.NODE_ENV !== 'test' && !data.defer) { 180 - data.defer = true; 181 - setTimeout(() => { 182 - initDataState('read', data, null); 183 - gc(); 184 - persistData(); 185 - clearDataState(); 186 - data.defer = false; 187 - }); 178 + if (process.env.NODE_ENV !== 'test') { 179 + // Schedule deferred tasks if we haven't already, and if either a persist or GC run 180 + // are likely to be needed 181 + if (!data.defer && (data.storage || !data.optimisticOrder.length)) { 182 + data.defer = true; 183 + setTimeout(() => { 184 + initDataState('read', data, null); 185 + gc(); 186 + persistData(); 187 + clearDataState(); 188 + data.defer = false; 189 + }); 190 + } 188 191 } 189 192 }; 190 193 ··· 230 233 persist: new Set(), 231 234 queryRootKey, 232 235 refCount: new Map(), 233 - refLock: new Map(), 234 236 links: { 235 237 optimistic: new Map(), 236 238 base: new Map(), ··· 241 243 }, 242 244 deferredKeys: new Set(), 243 245 commutativeKeys: new Set(), 246 + dirtyKeys: new Set(), 244 247 optimisticOrder: [], 245 248 storage: null, 246 249 }); ··· 314 317 }; 315 318 316 319 /** Adjusts the reference count of an entity on a refCount dict by "by" and updates the gc */ 317 - const updateRCForEntity = ( 318 - gc: undefined | Set<string>, 319 - refCount: KeyMap<number>, 320 - entityKey: string, 321 - by: number 322 - ): void => { 320 + const updateRCForEntity = (entityKey: string, by: number): void => { 323 321 // Retrieve the reference count and adjust it by "by" 324 - const count = refCount.get(entityKey) || 0; 325 - const newCount = count + by; 326 - refCount.set(entityKey, newCount); 322 + const count = currentData!.refCount.get(entityKey) || 0; 323 + const newCount = count + by > 0 ? count + by : 0; 324 + currentData!.refCount.set(entityKey, newCount); 327 325 // Add it to the garbage collection batch if it needs to be deleted or remove it 328 326 // from the batch if it needs to be kept 329 - if (gc) { 330 - if (newCount <= 0) gc.add(entityKey); 331 - else if (count <= 0 && newCount > 0) gc.delete(entityKey); 332 - } 327 + if (!newCount) currentData!.gc.add(entityKey); 328 + else if (!count && newCount) currentData!.gc.delete(entityKey); 333 329 }; 334 330 335 331 /** Adjusts the reference counts of all entities of a link on a refCount dict by "by" and updates the gc */ 336 - const updateRCForLink = ( 337 - gc: undefined | Set<string>, 338 - refCount: KeyMap<number>, 339 - link: Link | undefined, 340 - by: number 341 - ): void => { 342 - if (typeof link === 'string') { 343 - updateRCForEntity(gc, refCount, link, by); 344 - } else if (Array.isArray(link)) { 345 - for (let i = 0, l = link.length; i < l; i++) { 346 - if (Array.isArray(link[i])) { 347 - updateRCForLink(gc, refCount, link[i], by); 348 - } else if (link[i]) { 349 - updateRCForEntity(gc, refCount, link[i] as string, by); 350 - } 351 - } 332 + const updateRCForLink = (link: Link | undefined, by: number): void => { 333 + if (Array.isArray(link)) { 334 + for (let i = 0, l = link.length; i < l; i++) updateRCForLink(link[i], by); 335 + } else if (typeof link === 'string') { 336 + updateRCForEntity(link, by); 352 337 } 353 338 }; 354 339 ··· 391 376 392 377 /** Garbage collects all entities that have been marked as having no references */ 393 378 export const gc = () => { 379 + // If we're currently awaiting deferred results, abort GC run 380 + if (currentData!.optimisticOrder.length) return; 381 + 394 382 // Iterate over all entities that have been marked for deletion 395 383 // Entities have been marked for deletion in `updateRCForEntity` if 396 384 // their reference count dropped to 0 397 - const { gc: batch } = currentData!; 398 - for (const entityKey of batch.keys()) { 399 - // Check first whether the reference count is still 0 385 + for (const entityKey of currentData!.gc.keys()) { 386 + // Remove the current key from the GC batch 387 + currentData!.gc.delete(entityKey); 388 + 389 + // Check first whether the entity has any references, 390 + // if so, we skip it from the GC run 400 391 const rc = currentData!.refCount.get(entityKey) || 0; 401 - if (rc > 0) { 402 - batch.delete(entityKey); 403 - return; 404 - } 405 - 406 - // Each optimistic layer may also still contain some references to marked entities 407 - for (const layerKey of currentData!.refLock.keys()) { 408 - const refCount = currentData!.refLock.get(layerKey); 409 - if (refCount) { 410 - const locks = refCount.get(entityKey) || 0; 411 - // If the optimistic layer has any references to the entity, don't GC it, 412 - // otherwise delete the reference count from the optimistic layer 413 - if (locks > 0) return; 414 - refCount.delete(entityKey); 415 - } 416 - } 392 + if (rc > 0) continue; 417 393 418 394 // Delete the reference count, and delete the entity from the GC batch 419 395 currentData!.refCount.delete(entityKey); 420 - batch.delete(entityKey); 421 396 currentData!.records.base.delete(entityKey); 422 397 const linkNode = currentData!.links.base.get(entityKey); 423 398 if (linkNode) { 424 399 currentData!.links.base.delete(entityKey); 425 - for (const fieldKey in linkNode) { 426 - updateRCForLink(batch, currentData!.refCount, linkNode[fieldKey], -1); 427 - } 400 + for (const fieldKey in linkNode) updateRCForLink(linkNode[fieldKey], -1); 428 401 } 429 402 } 430 403 }; ··· 484 457 fieldKey: string, 485 458 link?: Link | undefined 486 459 ) => { 487 - const data = currentData!; 488 - // Retrieve the reference counting dict or the optimistic reference locking dict 489 - let refCount: KeyMap<number> | undefined; 490 - // Retrive the link NodeMap from either an optimistic or the base layer 491 - let links: KeyMap<Dict<Link | undefined>> | undefined; 492 - // Set the GC batch if we're not optimistically updating 493 - let gc: undefined | Set<string>; 494 - if (currentOptimisticKey) { 495 - // The refLock counters are also reference counters, but they prevent 496 - // garbage collection instead of being used to trigger it 497 - refCount = data.refLock.get(currentOptimisticKey); 498 - if (!refCount) 499 - data.refLock.set(currentOptimisticKey, (refCount = new Map())); 500 - links = data.links.optimistic.get(currentOptimisticKey); 501 - } else { 502 - refCount = data.refCount; 503 - links = data.links.base; 504 - gc = data.gc; 460 + // Retrieve the link NodeMap from either an optimistic or the base layer 461 + const links = currentOptimisticKey 462 + ? currentData!.links.optimistic.get(currentOptimisticKey) 463 + : currentData!.links.base; 464 + // Update the reference count for the link 465 + if (!currentOptimisticKey) { 466 + updateRCForLink(links?.get(entityKey)?.[fieldKey], -1); 467 + updateRCForLink(link, 1); 505 468 } 506 - 507 - // Retrieve the previous link for this field 508 - const prevLinkNode = links && links.get(entityKey); 509 - const prevLink = prevLinkNode && prevLinkNode[fieldKey]; 510 - 511 469 // Update persistence batch and dependencies 512 470 updateDependencies(entityKey, fieldKey); 513 471 updatePersist(entityKey, fieldKey); 514 472 // Update the link 515 - setNode(data.links, entityKey, fieldKey, link); 516 - // First decrease the reference count for the previous link 517 - updateRCForLink(gc, refCount, prevLink, -1); 518 - // Then increase the reference count for the new link 519 - updateRCForLink(gc, refCount, link, 1); 473 + setNode(currentData!.links, entityKey, fieldKey, link); 520 474 }; 521 475 522 476 /** Reserves an optimistic layer and preorders it */ ··· 538 492 index = index > -1 ? index : 0; 539 493 index < data.optimisticOrder.length && 540 494 !data.deferredKeys.has(data.optimisticOrder[index]) && 541 - (!data.refLock.has(data.optimisticOrder[index]) || 495 + (!data.dirtyKeys.has(data.optimisticOrder[index]) || 542 496 !data.commutativeKeys.has(data.optimisticOrder[index])); 543 497 index++ 544 498 ); ··· 563 517 data.optimisticOrder.unshift(layerKey); 564 518 } 565 519 566 - if (!data.refLock.has(layerKey)) { 567 - data.refLock.set(layerKey, new Map()); 520 + if (!data.dirtyKeys.has(layerKey)) { 521 + data.dirtyKeys.add(layerKey); 568 522 data.links.optimistic.set(layerKey, new Map()); 569 523 data.records.optimistic.set(layerKey, new Map()); 570 524 } ··· 572 526 573 527 /** Clears all links and records of an optimistic layer */ 574 528 const clearLayer = (data: InMemoryData, layerKey: number) => { 575 - if (data.refLock.has(layerKey)) { 576 - data.refLock.delete(layerKey); 529 + if (data.dirtyKeys.has(layerKey)) { 530 + data.dirtyKeys.delete(layerKey); 577 531 data.records.optimistic.delete(layerKey); 578 532 data.links.optimistic.delete(layerKey); 579 533 data.deferredKeys.delete(layerKey);