Repository navigation
Fix: WhereExcludes is O(n*m) and stalls the asset pool for large excluded albums - #706
Conversation
WhereExcludes rescanned the entire excluded collection for every item in source — O(n*m) — with the Func<T, object> comparator boxing a Guid on every comparison. ApplyAccountFilters is the only call site, and CachingApiAssetsPool caches the resulting lazy IEnumerable rather than a materialised list, so the full scan re-ran on every enumeration of the pool. That is invisible for a handful of excluded assets. With 30313 assets across four configured People and 3618 assets in an excluded album it works out to ~5.5e7 comparisons per enumeration, several enumerations per request: authenticated /api/Asset requests stopped returning inside 60s and the frame served nothing at all. Hash the excluded keys once and test membership instead. O(n+m), and one boxing allocation per item rather than per comparison. TagAssetsPool already uses a HashSet<Guid> for the same kind of lookup. Semantics are unchanged: EqualityComparer<object>.Default calls the same object.Equals the previous code did, and for a boxed Guid that is value equality backed by a consistent GetHashCode.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthrough
ChangesWhereExcludes filtering
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The exclusion lookup optimization preserves the demonstrated key-based behavior while avoiding repeated scans. No actionable merge risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
|
Looks good to me, what do you think @JW-CH ? |
Problem
WhereExcludesrescans the entire excluded collection for every item in the source:That is O(n×m), and allocation-heavy with it: the
Func<T, object>comparator boxes aGuidon every single comparison, on both sides.It has one call site,
AssetExtensionMethods.ApplyAccountFilters:and
CachingApiAssetsPoolcaches the resulting lazyIEnumerablerather than a materialised list, so the full scan re-runs on every enumeration of the pool.With a small
ExcludedAlbumsthis is invisible. On my library it isn't:PeopleShowVideoscould be turned on without a 20-minute clip parking the frame for 20 minutesThat works out to ~5.5×10⁷ comparisons per enumeration, several enumerations per request. Authenticated
/api/Assetrequests stopped returning entirely — they hung past a 60s timeout, so the frame served nothing rather than erroring, which made it look like an empty pool rather than a slow one. Dropping back to the 98-asset excluded album I had before (~1.5×10⁶ comparisons) made it instant again.Fix
Hash the excluded keys once, then test membership:
O(n+m), and boxing drops from one per comparison to one per item.
TagAssetsPoolalready uses aHashSet<Guid>for the same kind of lookup.Semantics are unchanged:
EqualityComparer<object>.Defaultcalls the sameobject.Equalsthe old code did, and for a boxedGuidthat is value equality backed by a consistentGetHashCode. Emptyexcludedand null elements behave as before.One deliberate behavioural detail: since the body is no longer a single lazy expression,
excludedis enumerated once whenWhereExcludesis called, instead of repeatedly on each enumeration of the result. The only call site passes an already-materialised list, and building the set eagerly is precisely what makes repeat enumerations of the cached pool cheap.Happy to go further and make the comparator generic (
Func<T, TKey>) to remove boxing altogether, but that changes the public signature so I left it out of this PR.Tests
Adds
ImmichFrame.Core.Tests/Helpers/CollectionExtensionMethodsTests.cs:excludedreturns everythingexcludedis enumerated once, not once per source itemThe last one is the regression guard. On
mainit fails withExpected: 1, But was: 100; the other three pass either way, which is the intended demonstration that the change is behaviour-preserving.Existing
AllAssetsPoolTests.GetAssets_ExcludesAssetsFromExcludedAlbumsand theAlbumAssetsPoolTestsexclusion cases cover the end-to-end semantics and are untouched.Both suites pass locally on .NET 8:
ImmichFrame.Core.Tests88/88 andImmichFrame.WebApi.Tests26/26.Summary by CodeRabbit
Bug Fixes
Tests