feat: optimize getSpeakerByYearAndId with O(1) Map lookup - #368
Conversation
Converts the linear O(N) array search inside getSpeakerByYearAndId to an O(1) Map lookup using a cached Map generation function. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe speaker hook now builds cached per-year ChangesSpeaker lookup optimization
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: dependency version conflict. Check your lock file or package.json. 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. Comment |
PR Summary by QodoOptimize speaker lookup via cached Map in getSpeakerByYearAndId
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
300 rules✅ Skills:
|
| const getSpeakersMap = cache(async (year: string | number): Promise<Map<string, Speaker>> => { | ||
| const speakers = await getSpeakers(year); | ||
| return speakers.find((speaker) => speaker.id === speakerId); | ||
| return new Map(speakers.map((speaker) => [speaker.id, speaker])); | ||
| }); |
There was a problem hiding this comment.
1. React cache mock collisions 🐞 Bug ☼ Reliability
With this PR, hooks/useSpeakers.ts now has two React.cache-wrapped functions that can be called with the same single argument (year), but the Jest mock for react.cache uses one shared cache keyed only by JSON.stringify(args). This can cause cached values for getSpeakers(year) and getSpeakersMap(year) to overwrite each other (wrong return types / order-dependent tests) under the mock implementation.
Agent Prompt
### Issue description
The Jest mock for `react.cache` in performance tests uses a single shared `cacheMap` keyed only by `JSON.stringify(args)`. After this PR adds `getSpeakersMap(year)` (also `cache`-wrapped) which calls `getSpeakers(year)`, the two cached functions can collide under the same key (e.g. `['2024']`), causing test-only incorrect behavior and potential flakiness.
### Issue Context
Production React `cache` scopes memoization per cached function; the current test mock does not, so adding a second cached function in the same module creates new collision opportunities.
### Fix Focus Areas
- __tests__/hooks_performance.test.ts[17-35]
- __tests__/schedule_performance.test.ts[13-31]
### Suggested fix
Update the mock to maintain a separate `Map` per wrapped function (e.g., create `const cacheMap = new Map()` inside the `cache: (fn) => { ... }` closure, or use a `WeakMap<Function, Map>` keyed by `fn`). If you need to clear caches between tests, track per-function maps and clear them in `beforeEach`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const speakers = await getSpeakers(year); | ||
| return speakers.find((speaker) => speaker.id === speakerId); | ||
| return new Map(speakers.map((speaker) => [speaker.id, speaker])); | ||
| }); |
There was a problem hiding this comment.
2. Duplicate ids change winner 🐞 Bug ≡ Correctness
getSpeakerByYearAndId now builds a Map from the speakers array, which overwrites earlier entries for duplicate speaker.id values; the previous .find() implementation returned the first match. If the fetched list contains duplicate IDs, the returned Speaker for the same (year, speakerId) will change.
Agent Prompt
### Issue description
`new Map(speakers.map(([id, speaker])))` keeps the *last* occurrence for a duplicate `speaker.id`, while the prior `.find()` returned the *first* occurrence. This is a subtle semantic change for duplicate IDs.
### Issue Context
Speaker ID uniqueness is not enforced in-repo (types only declare `id: string`). Defensive behavior should ideally remain stable.
### Fix Focus Areas
- hooks/useSpeakers.ts[38-41]
### Suggested fix
Build the map with a loop that preserves the first occurrence:
```ts
const map = new Map<string, Speaker>();
for (const speaker of speakers) {
if (!map.has(speaker.id)) map.set(speaker.id, speaker);
}
return map;
```
Optionally, detect duplicates and `console.warn` once per year to surface upstream data issues.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const getSpeakersMap = cache(async (year: string | number): Promise<Map<string, Speaker>> => { | ||
| const speakers = await getSpeakers(year); | ||
| return speakers.find((speaker) => speaker.id === speakerId); | ||
| return new Map(speakers.map((speaker) => [speaker.id, speaker])); | ||
| }); | ||
|
|
||
| export const getSpeakerByYearAndId = async (year: string | number, speakerId: string): Promise<Speaker | undefined> => { | ||
| const speakersMap = await getSpeakersMap(year); | ||
| return speakersMap.get(speakerId); |
There was a problem hiding this comment.
3. Map build removes early-exit 🐞 Bug ➹ Performance
getSpeakerByYearAndId now always constructs a full Map of all speakers for the year before doing a single lookup, eliminating the early-exit behavior of the previous .find() for one-off calls. This can add unnecessary CPU/memory on call sites that only need one speaker (e.g., OpenGraph image generation).
Agent Prompt
### Issue description
Building a `Map` requires iterating the entire speakers array and allocating the map (and currently also an intermediate array via `.map`). For single lookups, `.find()` can short-circuit and allocates less.
### Issue Context
Some call sites appear to do a single lookup per request (e.g. OG image generation), while other paths may do many lookups where a cached map helps.
### Fix Focus Areas
- hooks/useSpeakers.ts[38-46]
- app/[year]/speakers/[speakerId]/opengraph-image.tsx[16-20]
### Suggested fix
Consider keeping `getSpeakerByYearAndId` as a simple `.find()` on the cached speakers array, and introduce/use a separate exported helper for bulk lookups (e.g. `getSpeakersMap(year)` or `getSpeakerByYearAndIdBulk(year, ids)`), so only multi-lookup paths pay the map construction cost.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
💡 What: Refactored
getSpeakerByYearAndIdto use a cachedgetSpeakersMapfunction that generates aMap<string, Speaker>instead of linearly searching the raw array using.find().🎯 Why: In components or build-time functions that render or process lists of objects (like schedule sessions looking up multiple speakers), repeatedly calling
.find()on the same large array leads to redundant O(N) iterations. Converting the array to a Map once (memoized per-request via React'scache) transforms these lookups into O(1) operations, drastically reducing CPU cycles during data-heavy rendering paths.📊 Impact: Reduces time complexity of speaker lookups from O(N) to O(1) for subsequent lookups within the same React rendering lifecycle/request.
🔬 Measurement: Verified that tests (
npm run test) continue to pass andhooks_performance.test.tsconfirms data is still cached efficiently. The impact can be seen in reduced flamegraph time for components heavily iterating over sessions and resolving speaker IDs.PR created automatically by Jules for task 337368392178284315 started by @anyulled
Summary by CodeRabbit