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