Skip to content

perf: replace O(n²) lookups with Map/Dictionary indexes#205

Open
rlorenzo wants to merge 1 commit into
mainfrom
fix/n2-queries
Open

perf: replace O(n²) lookups with Map/Dictionary indexes#205
rlorenzo wants to merge 1 commit into
mainfrom
fix/n2-queries

Conversation

@rlorenzo

@rlorenzo rlorenzo commented May 20, 2026

Copy link
Copy Markdown
Contributor

Replaces O(n²) lookup patterns (nested .find()/.some()/.Any() inside .map()/.forEach()/foreach) with Map/Set/Dictionary indexes built once, then O(1) lookups in the loop.

Prompted by The O(n²) bug that looked like clean code.

Changes

  • ManageMilestones.vue (CTS): Set of milestone competency IDs
  • ScheduledCliWeeks.vue (Effort): per-instructor Map keyed by term name
  • StudentClassYear.vue (Students): Map of students by person ID
  • CompetencyController.cs (CTS): Dictionary of competencies by ID for parent lookup
  • CMSLinkCollectionLinks.cs (CMS): GroupBy/ToDictionary of links by tag value

Notes

  • The CMS grouped-links endpoint keys links by tag value in a dictionary, which can't hold a null key. Null-valued ("uncategorized") links are appended after the grouped links so they are not dropped from the response (preserves prior behavior while keeping the non-null lookups O(1)).
  • Read-only CMS queries now use AsNoTracking().

Tests

Adds backend tests for the competency hierarchy build (nesting + orphaned parent) and the CMS grouped-links ordering (grouping, de-dup, and the null-append path). The four CMS DbSets are marked virtual to allow NSubstitute mocking, mirroring the existing CTS context.

@rlorenzo

rlorenzo commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@codecov-commenter

codecov-commenter commented May 20, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 86 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
viper-frontend-esm 2.13MB 86 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/StudentClassYear-*.js 25 bytes 8.39kB 0.3%
assets/ManageMilestones-*.js 42 bytes 3.53kB 1.21%
assets/ScheduledCliWeeks-*.js 19 bytes 3.07kB 0.62%

Files in assets/StudentClassYear-*.js:

  • ./src/Students/pages/StudentClassYear.vue → Total Size: 156 bytes

Files in assets/ManageMilestones-*.js:

  • ./src/CTS/pages/ManageMilestones.vue → Total Size: 151 bytes

Files in assets/ScheduledCliWeeks-*.js:

  • ./src/Effort/pages/ScheduledCliWeeks.vue → Total Size: 157 bytes

@codecov-commenter

codecov-commenter commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 43.47%. Comparing base (185f8b2) to head (de69cd3).

Files with missing lines Patch % Lines
...eb/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs 94.73% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #205      +/-   ##
==========================================
+ Coverage   43.21%   43.47%   +0.25%     
==========================================
  Files         894      894              
  Lines       51851    51857       +6     
  Branches     4844     4845       +1     
==========================================
+ Hits        22406    22543     +137     
+ Misses      28899    28766     -133     
- Partials      546      548       +2     
Flag Coverage Δ
backend 43.57% <96.15%> (+0.27%) ⬆️
frontend 41.32% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Five files eliminate O(n) and O(n²) search patterns during data filtering, lookup, and grouping by precomputing Maps, Sets, and Dictionaries. Vue components optimize term and student resolution, while C# controllers optimize competency hierarchy construction and link grouping.

Changes

Lookup Performance Optimizations

Layer / File(s) Summary
Vue component lookup optimizations
VueApp/src/CTS/pages/ManageMilestones.vue, VueApp/src/Effort/pages/ScheduledCliWeeks.vue, VueApp/src/Students/pages/StudentClassYear.vue
Competencies filtering now uses a precomputed Set of existing milestone IDs; term resolution precomputes an instructor Map by termName; student association precomputes a Map keyed by personId instead of repeated find() calls.
C# controller hierarchy and grouping optimizations
web/Areas/CTS/Controllers/CompetencyController.cs, web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs
Competency hierarchy construction uses a CompetencyId → DTO dictionary for parent lookup instead of repeated FirstOrDefault searches; link grouping precomputes distinct tag values and a tag → links mapping to avoid per-link searches.

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing O(n²) lookups with optimized Map/Dictionary indexes across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description clearly explains the purpose (replacing O(n²) lookup patterns with indexed structures), provides specific file-by-file changes with context, and includes implementation notes addressing edge cases like null-keyed dictionary entries.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/n2-queries

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Improves performance across several endpoints/pages by replacing repeated linear searches with precomputed Map/Dictionary indexes, and updates CMS link grouping to avoid null-key dictionary failures.

Changes:

  • CTS Competency hierarchy: replace per-item parent lookup (FirstOrDefault) with an ID -> DTO dictionary.
  • CMS grouped links: pre-index links by tag value via GroupBy/ToDictionary and filter out null tag values to prevent Dictionary<string, …> null-key exceptions.
  • Vue pages: replace repeated .find()/.some() loops with Map/Set lookups.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/Areas/CTS/Controllers/CompetencyController.cs Builds a Dictionary for O(1) parent lookups when constructing competency hierarchies.
web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs Builds a tag-value index to order grouped link results efficiently; adds null filtering for dictionary keys.
VueApp/src/Students/pages/StudentClassYear.vue Uses a Map keyed by personId to apply problem data without repeated .find().
VueApp/src/Effort/pages/ScheduledCliWeeks.vue Uses a Map keyed by termName to avoid repeated .find() per term column.
VueApp/src/CTS/pages/ManageMilestones.vue Uses a Set of milestone competency IDs to filter competencies in O(n).

Comment thread web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs Outdated
Comment thread web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs
Comment thread web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs Fixed
Comment thread web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs Fixed
Comment thread web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs Fixed
Comment thread web/Areas/CMS/Controllers/CMSLinkCollectionLinks.cs Fixed
The CMS grouped-links endpoint indexes links by tag value, and a
dictionary cannot key on null. Null-valued ("uncategorized") links are
appended after the grouped links so they are not dropped, preserving the
prior response while keeping the non-null lookups O(1).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment on lines 67 to +71
.ThenBy(c => c.Order)
.ToListAsync();
var compHierarchy = new List<CompetencyHierarchyDto>();
var allCompDtos = CtsMapper.ToCompetencyHierarchyDtos(comps);
var compsById = allCompDtos.ToDictionary(c => c.CompetencyId);
Comment on lines 29 to +30
var links = await _context.Links
.AsNoTracking()
Comment on lines 67 to 69
var allValues = await _context.LinkTags
.AsNoTracking()
.Where(lt => lt.LinkCollectionTagCategoryId == tagCategories.LinkCollectionTagCategoryId)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants