enhance(normalizr): Pre-allocate dependency slot to avoid Array.unshift()#3876
enhance(normalizr): Pre-allocate dependency slot to avoid Array.unshift()#3876
Conversation
…ft() GlobalCache.getResults() called unshift() on every cache-miss denormalization, which is O(n) because it shifts all existing elements. Pre-allocate slot 0 with a placeholder and fill it in-place, turning the operation into O(1). Benchmarks showed 1.3–3.2% improvement on cold-denormalize paths (denormalizeLong variants). Made-with: Cursor
🦋 Changeset detectedLatest commit: 3eaf5f2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Size Change: +41 B (+0.05%) Total Size: 80.6 kB 📦 View Changed
ℹ️ View Unchanged
|
There was a problem hiding this comment.
Benchmark React
Details
| Benchmark suite | Current: 3eaf5f2 | Previous: f5797b4 | Ratio |
|---|---|---|---|
data-client: getlist-100 |
186.93 ops/s (± 4.2%) |
170.95 ops/s (± 4.9%) |
0.91 |
data-client: getlist-500 |
48.31 ops/s (± 3.4%) |
47.17 ops/s (± 9.3%) |
0.98 |
data-client: update-entity |
454.55 ops/s (± 5.1%) |
444.66 ops/s (± 3.5%) |
0.98 |
data-client: update-user |
555.56 ops/s (± 6.2%) |
416.67 ops/s (± 6.6%) |
0.75 |
data-client: getlist-500-sorted |
50.76 ops/s (± 4.3%) |
52.49 ops/s (± 5.9%) |
1.03 |
data-client: update-entity-sorted |
416.67 ops/s (± 4.7%) |
416.67 ops/s (± 5.0%) |
1 |
data-client: update-entity-multi-view |
416.67 ops/s (± 3.1%) |
400 ops/s (± 6.6%) |
0.96 |
data-client: list-detail-switch-10 |
12.79 ops/s (± 7.5%) |
11.06 ops/s (± 5.9%) |
0.86 |
data-client: update-user-10000 |
125.79 ops/s (± 5.7%) |
94.34 ops/s (± 0.8%) |
0.75 |
data-client: invalidate-and-resolve |
56.82 ops/s (± 4.1%) |
51.55 ops/s (± 3.4%) |
0.91 |
data-client: unshift-item |
322.58 ops/s (± 4.0%) |
303.03 ops/s (± 4.3%) |
0.94 |
data-client: delete-item |
434.78 ops/s (± 3.6%) |
400 ops/s (± 8.2%) |
0.92 |
data-client: move-item |
212.77 ops/s (± 8.1%) |
202.04 ops/s (± 6.1%) |
0.95 |
This comment was automatically generated by workflow using github-action-benchmark.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3876 +/- ##
=======================================
Coverage 98.10% 98.10%
=======================================
Files 153 153
Lines 2899 2904 +5
Branches 564 564
=======================================
+ Hits 2844 2849 +5
Misses 11 11
Partials 44 44 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: 3eaf5f2 | Previous: 467a5f6 | Ratio |
|---|---|---|---|
normalizeLong |
446 ops/sec (±1.40%) |
454 ops/sec (±1.16%) |
1.02 |
normalizeLong Values |
407 ops/sec (±0.27%) |
413 ops/sec (±0.34%) |
1.01 |
denormalizeLong |
283 ops/sec (±3.05%) |
291 ops/sec (±2.38%) |
1.03 |
denormalizeLong Values |
260 ops/sec (±2.35%) |
263 ops/sec (±2.52%) |
1.01 |
denormalizeLong donotcache |
994 ops/sec (±0.18%) |
1004 ops/sec (±0.18%) |
1.01 |
denormalizeLong Values donotcache |
745 ops/sec (±0.19%) |
751 ops/sec (±0.14%) |
1.01 |
denormalizeShort donotcache 500x |
1586 ops/sec (±0.13%) |
1599 ops/sec (±0.12%) |
1.01 |
denormalizeShort 500x |
838 ops/sec (±2.33%) |
849 ops/sec (±2.31%) |
1.01 |
denormalizeShort 500x withCache |
6301 ops/sec (±1.10%) |
6326 ops/sec (±0.11%) |
1.00 |
queryShort 500x withCache |
2718 ops/sec (±0.22%) |
2676 ops/sec (±0.44%) |
0.98 |
buildQueryKey All |
54089 ops/sec (±0.35%) |
54520 ops/sec (±0.33%) |
1.01 |
query All withCache |
6070 ops/sec (±0.26%) |
6705 ops/sec (±0.25%) |
1.10 |
denormalizeLong with mixin Entity |
279 ops/sec (±2.07%) |
275 ops/sec (±2.32%) |
0.99 |
denormalizeLong withCache |
7754 ops/sec (±0.22%) |
6826 ops/sec (±0.15%) |
0.88 |
denormalizeLong Values withCache |
5017 ops/sec (±0.12%) |
5043 ops/sec (±0.58%) |
1.01 |
denormalizeLong All withCache |
5848 ops/sec (±0.25%) |
6426 ops/sec (±0.24%) |
1.10 |
denormalizeLong Query-sorted withCache |
6140 ops/sec (±0.17%) |
6738 ops/sec (±0.13%) |
1.10 |
denormalizeLongAndShort withEntityCacheOnly |
1776 ops/sec (±0.28%) |
1677 ops/sec (±0.18%) |
0.94 |
denormalize bidirectional 50 |
5723 ops/sec (±2.16%) |
5868 ops/sec (±1.91%) |
1.03 |
denormalize bidirectional 50 donotcache |
42268 ops/sec (±0.45%) |
41809 ops/sec (±0.77%) |
0.99 |
getResponse |
4628 ops/sec (±0.76%) |
4524 ops/sec (±0.62%) |
0.98 |
getResponse (null) |
10704259 ops/sec (±0.70%) |
10391914 ops/sec (±1.16%) |
0.97 |
getResponse (clear cache) |
265 ops/sec (±2.06%) |
267 ops/sec (±1.99%) |
1.01 |
getSmallResponse |
3305 ops/sec (±1.13%) |
3161 ops/sec (±1.56%) |
0.96 |
getSmallInferredResponse |
2523 ops/sec (±0.22%) |
2443 ops/sec (±0.51%) |
0.97 |
getResponse Collection |
4528 ops/sec (±0.50%) |
4560 ops/sec (±0.38%) |
1.01 |
get Collection |
4703 ops/sec (±0.30%) |
4567 ops/sec (±0.50%) |
0.97 |
get Query-sorted |
5239 ops/sec (±0.19%) |
5219 ops/sec (±0.24%) |
1.00 |
setLong |
457 ops/sec (±0.22%) |
458 ops/sec (±0.30%) |
1.00 |
setLongWithMerge |
258 ops/sec (±0.24%) |
257 ops/sec (±0.25%) |
1.00 |
setLongWithSimpleMerge |
271 ops/sec (±0.45%) |
273 ops/sec (±0.43%) |
1.01 |
setSmallResponse 500x |
944 ops/sec (±0.19%) |
934 ops/sec (±0.21%) |
0.99 |
This comment was automatically generated by workflow using github-action-benchmark.
Motivation
GlobalCache.getResults()callsunshift()to prepend the input reference to the dependency tracking array on every cache-miss denormalization.Array.prototype.unshift()is O(n) — it shifts every existing element right by one index. As the dependency array grows (especially for large normalized responses), this becomes a measurable bottleneck.Identified via V8 profiling of the React benchmark (
examples/benchmark-reactwithBENCH_V8_DEOPT=true).Solution
Pre-allocate slot 0 with a
PLACEHOLDER_DEPsentinel when theGlobalCacheis constructed. IngetResults(), replace theunshift()with a direct assignment todependencies[0], turning the operation from O(n) to O(1). Thepaths()method is updated to skip slot 0 (the input reference entry) when extracting entity paths.Benchmark results (isolated, React benchmark
denormalizeLongvariants):denormalizeLong (cold): +1.3–3.2% ops/sec improvementunshiftwas called there)Open questions
N/A
Made with Cursor
Note
Medium Risk
Touches
GlobalCachedependency/path tracking used to populate endpoint result caches; an off-by-one or placeholder misuse could cause subtle cache invalidation issues despite being a small change.Overview
Improves denormalization cache-miss performance in
GlobalCache.getResults()by removingArray.prototype.unshift()and instead pre-allocating dependency slot0(filled with the input reference on misses).Updates
paths()to skip the reserved slot and build the returned path list without including the input placeholder, and adds a changeset to publish this as a patch performance improvement.Reviewed by Cursor Bugbot for commit 3eaf5f2. Bugbot is set up for automated code reviews on this repo. Configure here.