1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.recyclerview.widget;
17 
18 import android.annotation.SuppressLint;
19 import android.os.Trace;
20 import android.view.View;
21 
22 import androidx.core.os.TraceCompat;
23 
24 import org.jspecify.annotations.Nullable;
25 
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.Comparator;
30 import java.util.concurrent.TimeUnit;
31 
32 final class GapWorker implements Runnable {
33 
34     static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
35 
36     ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();
37     long mPostTimeNs;
38     long mFrameIntervalNs;
39 
40     static class Task {
41         public boolean neededNextFrame;
42         public int viewVelocity;
43         public int distanceToItem;
44         public RecyclerView view;
45         public int position;
46 
clear()47         public void clear() {
48             neededNextFrame = false;
49             viewVelocity = 0;
50             distanceToItem = 0;
51             view = null;
52             position = 0;
53         }
54     }
55 
56     /**
57      * Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects
58      * are pooled in the ArrayList, and never removed to avoid allocations, but always cleared
59      * in between calls.
60      */
61     private final ArrayList<Task> mTasks = new ArrayList<>();
62 
63     /**
64      * Prefetch information associated with a specific RecyclerView.
65      */
66     @SuppressLint("VisibleForTests")
67     static class LayoutPrefetchRegistryImpl
68             implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {
69         int mPrefetchDx;
70         int mPrefetchDy;
71         int[] mPrefetchArray;
72 
73         int mCount;
74 
setPrefetchVector(int dx, int dy)75         void setPrefetchVector(int dx, int dy) {
76             mPrefetchDx = dx;
77             mPrefetchDy = dy;
78         }
79 
collectPrefetchPositionsFromView(RecyclerView view, boolean nested)80         void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
81             mCount = 0;
82             if (mPrefetchArray != null) {
83                 Arrays.fill(mPrefetchArray, -1);
84             }
85 
86             final RecyclerView.LayoutManager layout = view.mLayout;
87             if (view.mAdapter != null
88                     && layout != null
89                     && layout.isItemPrefetchEnabled()) {
90                 if (nested) {
91                     // nested prefetch, only if no adapter updates pending. Note: we don't query
92                     // view.hasPendingAdapterUpdates(), as first layout may not have occurred
93                     if (!view.mAdapterHelper.hasPendingUpdates()) {
94                         layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
95                     }
96                 } else {
97                     // momentum based prefetch, only if we trust current child/adapter state
98                     if (!view.hasPendingAdapterUpdates()) {
99                         layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
100                                 view.mState, this);
101                     }
102                 }
103 
104                 if (mCount > layout.mPrefetchMaxCountObserved) {
105                     layout.mPrefetchMaxCountObserved = mCount;
106                     layout.mPrefetchMaxObservedInInitialPrefetch = nested;
107                     view.mRecycler.updateViewCacheSize();
108                 }
109             }
110         }
111 
112         @Override
addPosition(int layoutPosition, int pixelDistance)113         public void addPosition(int layoutPosition, int pixelDistance) {
114             if (layoutPosition < 0) {
115                 throw new IllegalArgumentException("Layout positions must be non-negative");
116             }
117 
118             if (pixelDistance < 0) {
119                 throw new IllegalArgumentException("Pixel distance must be non-negative");
120             }
121 
122             // allocate or expand array as needed, doubling when needed
123             final int storagePosition = mCount * 2;
124             if (mPrefetchArray == null) {
125                 mPrefetchArray = new int[4];
126                 Arrays.fill(mPrefetchArray, -1);
127             } else if (storagePosition >= mPrefetchArray.length) {
128                 final int[] oldArray = mPrefetchArray;
129                 mPrefetchArray = new int[storagePosition * 2];
130                 System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
131             }
132 
133             // add position
134             mPrefetchArray[storagePosition] = layoutPosition;
135             mPrefetchArray[storagePosition + 1] = pixelDistance;
136 
137             mCount++;
138         }
139 
lastPrefetchIncludedPosition(int position)140         boolean lastPrefetchIncludedPosition(int position) {
141             if (mPrefetchArray != null) {
142                 final int count = mCount * 2;
143                 for (int i = 0; i < count; i += 2) {
144                     if (mPrefetchArray[i] == position) return true;
145                 }
146             }
147             return false;
148         }
149 
150         /**
151          * Called when prefetch indices are no longer valid for cache prioritization.
152          */
clearPrefetchPositions()153         void clearPrefetchPositions() {
154             if (mPrefetchArray != null) {
155                 Arrays.fill(mPrefetchArray, -1);
156             }
157             mCount = 0;
158         }
159     }
160 
add(RecyclerView recyclerView)161     public void add(RecyclerView recyclerView) {
162         if (RecyclerView.sDebugAssertionsEnabled && mRecyclerViews.contains(recyclerView)) {
163             throw new IllegalStateException("RecyclerView already present in worker list!");
164         }
165         mRecyclerViews.add(recyclerView);
166     }
167 
remove(RecyclerView recyclerView)168     public void remove(RecyclerView recyclerView) {
169         boolean removeSuccess = mRecyclerViews.remove(recyclerView);
170         if (RecyclerView.sDebugAssertionsEnabled && !removeSuccess) {
171             throw new IllegalStateException("RecyclerView removal failed!");
172         }
173     }
174 
175     /**
176      * Schedule a prefetch immediately after the current traversal.
177      */
postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy)178     void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
179         if (recyclerView.isAttachedToWindow()) {
180             if (RecyclerView.sDebugAssertionsEnabled && !mRecyclerViews.contains(recyclerView)) {
181                 throw new IllegalStateException("attempting to post unregistered view!");
182             }
183             if (mPostTimeNs == 0) {
184                 mPostTimeNs = recyclerView.getNanoTime();
185                 recyclerView.post(this);
186             }
187         }
188 
189         recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
190     }
191 
192     static Comparator<Task> sTaskComparator = new Comparator<Task>() {
193         @Override
194         public int compare(Task lhs, Task rhs) {
195             // first, prioritize non-cleared tasks
196             if ((lhs.view == null) != (rhs.view == null)) {
197                 return lhs.view == null ? 1 : -1;
198             }
199 
200             // then prioritize those (we think) are needed for next frame
201             if (lhs.neededNextFrame != rhs.neededNextFrame) {
202                 return lhs.neededNextFrame ? -1 : 1;
203             }
204 
205             // then prioritize _highest_ view velocity
206             int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity;
207             if (deltaViewVelocity != 0) return deltaViewVelocity;
208 
209             // then prioritize _lowest_ distance to item
210             int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem;
211             if (deltaDistanceToItem != 0) return deltaDistanceToItem;
212 
213             return 0;
214         }
215     };
216 
buildTaskList()217     private void buildTaskList() {
218         // Update PrefetchRegistry in each view
219         final int viewCount = mRecyclerViews.size();
220         int totalTaskCount = 0;
221         for (int i = 0; i < viewCount; i++) {
222             RecyclerView view = mRecyclerViews.get(i);
223             if (view.getWindowVisibility() == View.VISIBLE) {
224                 view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
225                 totalTaskCount += view.mPrefetchRegistry.mCount;
226             }
227         }
228 
229         // Populate task list from prefetch data...
230         mTasks.ensureCapacity(totalTaskCount);
231         int totalTaskIndex = 0;
232         for (int i = 0; i < viewCount; i++) {
233             RecyclerView view = mRecyclerViews.get(i);
234             if (view.getWindowVisibility() != View.VISIBLE) {
235                 // Invisible view, don't bother prefetching
236                 continue;
237             }
238 
239             LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
240             final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
241                     + Math.abs(prefetchRegistry.mPrefetchDy);
242             for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
243                 final Task task;
244                 if (totalTaskIndex >= mTasks.size()) {
245                     task = new Task();
246                     mTasks.add(task);
247                 } else {
248                     task = mTasks.get(totalTaskIndex);
249                 }
250                 final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
251 
252                 task.neededNextFrame = distanceToItem <= viewVelocity;
253                 task.viewVelocity = viewVelocity;
254                 task.distanceToItem = distanceToItem;
255                 task.view = view;
256                 task.position = prefetchRegistry.mPrefetchArray[j];
257 
258                 totalTaskIndex++;
259             }
260         }
261 
262         // ... and priority sort
263         Collections.sort(mTasks, sTaskComparator);
264     }
265 
isPrefetchPositionAttached(RecyclerView view, int position)266     static boolean isPrefetchPositionAttached(RecyclerView view, int position) {
267         final int childCount = view.mChildHelper.getUnfilteredChildCount();
268         for (int i = 0; i < childCount; i++) {
269             View attachedView = view.mChildHelper.getUnfilteredChildAt(i);
270             RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView);
271             // Note: can use mPosition here because adapter doesn't have pending updates
272             if (holder.mPosition == position && !holder.isInvalid()) {
273                 return true;
274             }
275         }
276         return false;
277     }
278 
prefetchPositionWithDeadline(RecyclerView view, int position, long deadlineNs)279     private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
280             int position, long deadlineNs) {
281         if (isPrefetchPositionAttached(view, position)) {
282             // don't attempt to prefetch attached views
283             return null;
284         }
285 
286         RecyclerView.Recycler recycler = view.mRecycler;
287         RecyclerView.ViewHolder holder;
288         try {
289             // FOREVER_NS is used as a deadline to force the work to occur now,
290             // since it's needed next frame, even if it won't fit in gap
291             if (deadlineNs == RecyclerView.FOREVER_NS && TraceCompat.isEnabled()) {
292                 Trace.beginSection("RV Prefetch forced - needed next frame");
293             }
294             view.onEnterLayoutOrScroll();
295             holder = recycler.tryGetViewHolderForPositionByDeadline(
296                     position, false, deadlineNs);
297 
298             if (holder != null) {
299                 if (holder.isBound() && !holder.isInvalid()) {
300                     // Only give the view a chance to go into the cache if binding succeeded
301                     // Note that we must use public method, since item may need cleanup
302                     recycler.recycleView(holder.itemView);
303                 } else {
304                     // Didn't bind, so we can't cache the view, but it will stay in the pool until
305                     // next prefetch/traversal. If a View fails to bind, it means we didn't have
306                     // enough time prior to the deadline (and won't for other instances of this
307                     // type, during this GapWorker prefetch pass).
308                     recycler.addViewHolderToRecycledViewPool(holder, false);
309                 }
310             }
311         } finally {
312             view.onExitLayoutOrScroll(false);
313             Trace.endSection();
314         }
315         return holder;
316     }
317 
prefetchInnerRecyclerViewWithDeadline(@ullable RecyclerView innerView, long deadlineNs)318     private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView,
319             long deadlineNs) {
320         if (innerView == null) {
321             return;
322         }
323 
324         if (innerView.mDataSetHasChangedAfterLayout
325                 && innerView.mChildHelper.getUnfilteredChildCount() != 0) {
326             // RecyclerView has new data, but old attached views. Clear everything, so that
327             // we can prefetch without partially stale data.
328             innerView.removeAndRecycleViews();
329         }
330 
331         // do nested prefetch!
332         final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry;
333         innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true);
334 
335         if (innerPrefetchRegistry.mCount != 0) {
336             try {
337                 Trace.beginSection(deadlineNs == RecyclerView.FOREVER_NS
338                         ? "RV Nested Prefetch"
339                         : "RV Nested Prefetch forced - needed next frame");
340                 innerView.mState.prepareForNestedPrefetch(innerView.mAdapter);
341                 for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) {
342                     // Note that we ignore immediate flag for inner items because
343                     // we have lower confidence they're needed next frame.
344                     final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i];
345                     prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs);
346                 }
347             } finally {
348                 Trace.endSection();
349             }
350         }
351     }
352 
flushTaskWithDeadline(Task task, long deadlineNs)353     private void flushTaskWithDeadline(Task task, long deadlineNs) {
354         long taskDeadlineNs = task.neededNextFrame ? RecyclerView.FOREVER_NS : deadlineNs;
355         RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
356                 task.position, taskDeadlineNs);
357         if (holder != null
358                 && holder.mNestedRecyclerView != null
359                 && holder.isBound()
360                 && !holder.isInvalid()) {
361             prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
362         }
363     }
364 
flushTasksWithDeadline(long deadlineNs)365     private void flushTasksWithDeadline(long deadlineNs) {
366         for (int i = 0; i < mTasks.size(); i++) {
367             final Task task = mTasks.get(i);
368             if (task.view == null) {
369                 break; // done with populated tasks
370             }
371             flushTaskWithDeadline(task, deadlineNs);
372             task.clear();
373         }
374     }
375 
prefetch(long deadlineNs)376     void prefetch(long deadlineNs) {
377         buildTaskList();
378         flushTasksWithDeadline(deadlineNs);
379     }
380 
381     @Override
run()382     public void run() {
383         try {
384             Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
385 
386             if (mRecyclerViews.isEmpty()) {
387                 // abort - no work to do
388                 return;
389             }
390 
391             // Query most recent vsync so we can predict next one. Note that drawing time not yet
392             // valid in animation/input callbacks, so query it here to be safe.
393             final int size = mRecyclerViews.size();
394             long latestFrameVsyncMs = 0;
395             for (int i = 0; i < size; i++) {
396                 RecyclerView view = mRecyclerViews.get(i);
397                 if (view.getWindowVisibility() == View.VISIBLE) {
398                     latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
399                 }
400             }
401 
402             if (latestFrameVsyncMs == 0) {
403                 // abort - either no views visible, or couldn't get last vsync for estimating next
404                 return;
405             }
406 
407             long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
408 
409             prefetch(nextFrameNs);
410 
411             // TODO: consider rescheduling self, if there's more work to do
412         } finally {
413             mPostTimeNs = 0;
414             Trace.endSection();
415         }
416     }
417 }
418