[AI Task] [Tizen.NUI.Components] Remove unrealizedItems temp list in Linear/GridLayouter scroll path#7644
[AI Task] [Tizen.NUI.Components] Remove unrealizedItems temp list in Linear/GridLayouter scroll path#7644JoonghyunCho wants to merge 2 commits into
Conversation
…r/GridLayouter Inline the unrealize check directly into VisibleItems.RemoveAll predicate in four scroll hot-path / NotifyItemRangeRemoved sites: - LinearLayouter.cs (RequestLayout, NotifyItemRangeRemoved) - GridLayouter.cs (RequestLayout, NotifyItemRangeRemoved) Before: allocated a temporary List<RecyclerViewItem> per call and used RemoveAll(list.Contains) (O(N*M) linear scan on each predicate call). After: single O(N) RemoveAll pass with zero allocation per call. No public API change. Behavior preserved. Fixes #7608
There was a problem hiding this comment.
Code Review
This pull request optimizes the removal of invisible or removed items in GridLayouter and LinearLayouter by replacing temporary list allocations and O(N*M) lookups with the RemoveAll method. While this improves performance to O(N), the review highlights that the current implementation still incurs heap allocations due to closure captures in the lambdas. To achieve true zero allocation on these performance-critical paths, a manual iteration and shifting approach was suggested.
| VisibleItems.RemoveAll(item => | ||
| { | ||
| if (item.Index < FirstVisible || item.Index > LastVisible) | ||
| { | ||
| //Console.WriteLine("[NUI] Unrealize{0}!", item.Index); | ||
| unrealizedItems.Add(item); | ||
| collectionView.UnrealizeItem(item); | ||
| return true; | ||
| } | ||
| } | ||
| VisibleItems.RemoveAll(unrealizedItems.Contains); | ||
| return false; | ||
| }); |
There was a problem hiding this comment.
While this change significantly improves performance by moving from O(N×M) to O(N), it doesn't strictly achieve "zero per-call allocation" as stated in the PR description. In C#, a lambda that captures local variables or this (which happens here via FirstVisible, LastVisible, and collectionView) results in a heap allocation for a closure object and a delegate instance on every call.
Since RequestLayout is on the scroll hot path, you could achieve true zero allocation by using a manual for loop to shift elements in-place, followed by a RemoveRange call. However, RemoveAll is much more idiomatic and readable, so this is only worth changing if absolute zero allocation is required for this performance-critical path.
| VisibleItems.RemoveAll(item => | ||
| { | ||
| if (item.Index < FirstVisible || item.Index > LastVisible) | ||
| { | ||
| unrealizedItems.Add(item); | ||
| collectionView.UnrealizeItem(item); | ||
| return true; | ||
| } | ||
| } | ||
| VisibleItems.RemoveAll(unrealizedItems.Contains); | ||
| unrealizedItems.Clear(); | ||
| return false; | ||
| }); |
There was a problem hiding this comment.
Similar to the change in GridLayouter, this RemoveAll call involves a closure allocation because the lambda captures state. To completely eliminate heap pressure on the scroll hot path, a manual iteration and shifting approach would be required. Given the PR's focus on eliminating allocations, this is a relevant detail to consider.
| VisibleItems.RemoveAll(item => | ||
| { | ||
| if ((item.Index >= startIndex) | ||
| && (item.Index < startIndex + count)) | ||
| { | ||
| unrealizedItems.Add(item); | ||
| collectionView.UnrealizeItem(item); | ||
| return true; | ||
| } | ||
| else if (item.Index >= startIndex + count) | ||
| if (item.Index >= startIndex + count) | ||
| { | ||
| item.Index -= count; | ||
| } | ||
| } | ||
| VisibleItems.RemoveAll(unrealizedItems.Contains); | ||
| unrealizedItems.Clear(); | ||
| return false; | ||
| }); |
There was a problem hiding this comment.
🤖 [AI Review]
🟡 Suggestion: The lambda still captures startIndex, count, and collectionView, forcing a per-call closure allocation — so the PR's stated "zero per-call allocation" goal is not fully achieved here. A reverse-iteration for loop with RemoveAt eliminates both the temp list and the closure with no behavior change.
| VisibleItems.RemoveAll(item => | |
| { | |
| if ((item.Index >= startIndex) | |
| && (item.Index < startIndex + count)) | |
| { | |
| unrealizedItems.Add(item); | |
| collectionView.UnrealizeItem(item); | |
| return true; | |
| } | |
| else if (item.Index >= startIndex + count) | |
| if (item.Index >= startIndex + count) | |
| { | |
| item.Index -= count; | |
| } | |
| } | |
| VisibleItems.RemoveAll(unrealizedItems.Contains); | |
| unrealizedItems.Clear(); | |
| return false; | |
| }); | |
| for (int i = VisibleItems.Count - 1; i >= 0; i--) | |
| { | |
| RecyclerViewItem item = VisibleItems[i]; | |
| if (item.Index >= startIndex && item.Index < startIndex + count) | |
| { | |
| collectionView.UnrealizeItem(item); | |
| VisibleItems.RemoveAt(i); | |
| } | |
| else if (item.Index >= startIndex + count) | |
| { | |
| item.Index -= count; | |
| } | |
| } |
There was a problem hiding this comment.
🤖 [AI Review]
Addressed in 03a8955 — replaced RemoveAll(lambda) with a reverse for loop using RemoveAt, eliminating both the temp list and the closure allocation on this path.
Replace RemoveAll(lambda) with reverse for + RemoveAt in four Linear/GridLayouter sites to eliminate the per-call closure allocation noted by reviewers, fully realizing the PR's "zero per-call allocation" goal on the scroll hot path. Applied-Human-Comments: 3254514563,3254514566 Applied-AI-Comments: 3255636218
|
🤖 [AI Review] |
Summary
Removes the temporary
List<RecyclerViewItem>allocation and the O(N×M)RemoveAll(list.Contains)pattern from four scroll/range-removed sites inLinearLayouterandGridLayouter. The unrealize check is inlined directly into theRemoveAllpredicate, giving a single O(N) pass with zero per-call allocation on the scroll hot path.Changes
src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/LinearLayouter.csRequestLayout(line ~422): replaced foreach-into-temp-list +RemoveAll(list.Contains)with inline-predicateRemoveAll.NotifyItemRangeRemoved(line ~1137): same replacement; theitem.Index -= countshift is folded into the same predicate.src/Tizen.NUI.Components/Controls/RecyclerView/Layouter/GridLayouter.csRequestLayout(line ~419): same replacement.NotifyItemRangeRemoved(line ~1032): same replacement.Mode
Refactoring
Performance impact (per call)
List<>allocationRemoveAllpredicateVisibleItemsRequestLayoutis called many times per second on scroll, so this directly removes per-frame Gen0 pressure on the recycler hot path.API compatibility
Index -= countshift for items above the removed range.Verification
dotnet build src/Tizen.NUI.Components— 0 errors; pre-existing warnings unchanged).sdbdevice available in this environment — manual on-device benchmark recommended).Fixes #7608