1 /*
<lambda>null2  * Copyright 2021 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 androidx.compose.foundation.lazy.layout
18 
19 import androidx.annotation.VisibleForTesting
20 import androidx.collection.mutableScatterMapOf
21 import androidx.compose.foundation.ComposeFoundationFlags
22 import androidx.compose.foundation.ComposeFoundationFlags.isAutomaticNestedPrefetchEnabled
23 import androidx.compose.foundation.ExperimentalFoundationApi
24 import androidx.compose.foundation.internal.checkPrecondition
25 import androidx.compose.foundation.internal.requirePrecondition
26 import androidx.compose.foundation.internal.requirePreconditionNotNull
27 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.LazyLayoutPrefetchResultScope
28 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState.PrefetchHandle
29 import androidx.compose.runtime.Stable
30 import androidx.compose.ui.Modifier
31 import androidx.compose.ui.layout.SubcomposeLayoutState
32 import androidx.compose.ui.node.ModifierNodeElement
33 import androidx.compose.ui.node.TraversableNode
34 import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction
35 import androidx.compose.ui.platform.InspectorInfo
36 import androidx.compose.ui.unit.Constraints
37 import androidx.compose.ui.unit.IntSize
38 import androidx.compose.ui.util.fastForEach
39 import androidx.compose.ui.util.trace
40 import androidx.compose.ui.util.traceValue
41 import kotlin.time.TimeSource.Monotonic.markNow
42 
43 /**
44  * State for lazy items prefetching, used by lazy layouts to instruct the prefetcher.
45  *
46  * Note: this class is a part of [LazyLayout] harness that allows for building custom lazy layouts.
47  * LazyLayout and all corresponding APIs are still under development and are subject to change.
48  *
49  * @param prefetchScheduler the [PrefetchScheduler] implementation to use to execute prefetch
50  *   requests. If null is provided, the default [PrefetchScheduler] for the platform will be used.
51  * @param onNestedPrefetch a callback which will be invoked when this LazyLayout is prefetched in
52  *   context of a parent LazyLayout, giving a chance to recursively prefetch its own children. See
53  *   [NestedPrefetchScope].
54  */
55 @ExperimentalFoundationApi
56 @Stable
57 class LazyLayoutPrefetchState(
58     internal val prefetchScheduler: PrefetchScheduler? = null,
59     private val onNestedPrefetch: (NestedPrefetchScope.() -> Unit)? = null
60 ) {
61 
62     private val prefetchMetrics: PrefetchMetrics = PrefetchMetrics()
63     internal var prefetchHandleProvider: PrefetchHandleProvider? = null
64 
65     /**
66      * The nested prefetch count after collecting and averaging ideal counts for multiple lazy
67      * layouts
68      */
69     internal var realizedNestedPrefetchCount: Int = UnspecifiedNestedPrefetchCount
70 
71     /**
72      * The ideal nested prefetch count this Lazy Layout would like to have prefetched as part of
73      * nested prefetching (e.g. number of visible items)
74      */
75     internal var idealNestedPrefetchCount = UnspecifiedNestedPrefetchCount
76 
77     /** The number of items that were nested prefetched in the most recent nested prefetch pass. */
78     internal var lastNumberOfNestedPrefetchItems = 0
79 
80     /**
81      * Schedules precomposition for the new item. If you also want to premeasure the item please use
82      * a second overload accepting a [Constraints] param.
83      *
84      * @param index item index to prefetch.
85      */
86     @Deprecated(
87         "Please use schedulePrecomposition(index) instead",
88         level = DeprecationLevel.WARNING
89     )
90     fun schedulePrefetch(index: Int): PrefetchHandle {
91         return prefetchHandleProvider?.schedulePrecomposition(
92             index,
93             true,
94             prefetchMetrics,
95         ) ?: DummyHandle
96     }
97 
98     /**
99      * Schedules precomposition for the new item. If you also want to premeasure the item please use
100      * [schedulePrecompositionAndPremeasure] instead. This function should only be called once per
101      * item. If the item has already been composed at the time this request executes, either from a
102      * previous call to this function or because the item is already visible, this request should
103      * have no meaningful effect.
104      *
105      * @param index item index to prefetch.
106      */
107     fun schedulePrecomposition(index: Int): PrefetchHandle = schedulePrecomposition(index, true)
108 
109     /**
110      * Internal implementation only. Schedules precomposition for the new item. If you also want to
111      * premeasure the item please use [schedulePrecompositionAndPremeasure] instead. This function
112      * should only be called once per item. If the item has already been composed at the time this
113      * request executes, either from a previous call to this function or because the item is already
114      * visible, this request should have no meaningful effect.
115      *
116      * @param index item index to prefetch.
117      * @param isHighPriority If this request is high priority. High priority requests are executed
118      *   in the order they're scheduled, but will take precedence over low priority requests.
119      */
120     internal fun schedulePrecomposition(index: Int, isHighPriority: Boolean): PrefetchHandle {
121         return prefetchHandleProvider?.schedulePrecomposition(
122             index,
123             isHighPriority,
124             prefetchMetrics,
125         ) ?: DummyHandle
126     }
127 
128     /**
129      * Schedules precomposition and premeasure for the new item.
130      *
131      * @param index item index to prefetch.
132      * @param constraints [Constraints] to use for premeasuring.
133      */
134     @Deprecated(
135         "Please use schedulePremeasure(index, constraints) instead",
136         level = DeprecationLevel.WARNING
137     )
138     fun schedulePrefetch(index: Int, constraints: Constraints): PrefetchHandle =
139         schedulePrecompositionAndPremeasure(index, constraints, null)
140 
141     /**
142      * Schedules precomposition and premeasure for the new item. This should be used instead of
143      * [schedulePrecomposition] if you also want to premeasure the item. This function should only
144      * be called once per item. If the item has already been composed / measured at the time this
145      * request executes, either from a previous call to this function or because the item is already
146      * visible, this request should have no meaningful effect.
147      *
148      * @param index item index to prefetch.
149      * @param constraints [Constraints] to use for premeasuring.
150      * @param onItemPremeasured This callback is called when the item premeasuring is finished. If
151      *   the request is canceled or no measuring is performed this callback won't be called. Use
152      *   [LazyLayoutPrefetchResultScope.getSize] to get the item's size.
153      */
154     fun schedulePrecompositionAndPremeasure(
155         index: Int,
156         constraints: Constraints,
157         onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)? = null
158     ): PrefetchHandle =
159         schedulePrecompositionAndPremeasure(index, constraints, true, onItemPremeasured)
160 
161     /**
162      * Internal implementation only. Schedules precomposition and premeasure for the new item. This
163      * should be used instead of [schedulePrecomposition] if you also want to premeasure the item.
164      * This function should only be called once per item. If the item has already been composed /
165      * measured at the time this request executes, either from a previous call to this function or
166      * because the item is already visible, this request should have no meaningful effect.
167      *
168      * @param index item index to prefetch.
169      * @param constraints [Constraints] to use for premeasuring.
170      * @param isHighPriority If this request is high priority. High priority requests are executed
171      *   in the order they're scheduled, but will take precedence over low priority requests.
172      * @param onItemPremeasured This callback is called when the item premeasuring is finished. If
173      *   the request is canceled or no measuring is performed this callback won't be called. Use
174      *   [LazyLayoutPrefetchResultScope.getSize] to get the item's size.
175      */
176     internal fun schedulePrecompositionAndPremeasure(
177         index: Int,
178         constraints: Constraints,
179         isHighPriority: Boolean,
180         onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)? = null
181     ): PrefetchHandle {
182         return prefetchHandleProvider?.schedulePremeasure(
183             index,
184             constraints,
185             prefetchMetrics,
186             isHighPriority,
187             onItemPremeasured
188         ) ?: DummyHandle
189     }
190 
191     internal fun collectNestedPrefetchRequests(): List<PrefetchRequest> {
192         val onNestedPrefetch = onNestedPrefetch ?: return emptyList()
193 
194         return NestedPrefetchScopeImpl(realizedNestedPrefetchCount)
195             .run {
196                 onNestedPrefetch()
197                 requests
198             }
199             .also {
200                 // save the number of nested prefetch items we used
201                 lastNumberOfNestedPrefetchItems = it.size
202             }
203     }
204 
205     sealed interface PrefetchHandle {
206         /**
207          * Notifies the prefetcher that previously scheduled item is no longer needed. If the item
208          * was precomposed already it will be disposed.
209          */
210         fun cancel()
211 
212         /**
213          * Marks this prefetch request as urgent, which is a way to communicate that the requested
214          * item is expected to be needed during the next frame.
215          *
216          * For urgent requests we can proceed with doing the prefetch even if the available time in
217          * the frame is less than we spend on similar prefetch requests on average.
218          */
219         fun markAsUrgent()
220     }
221 
222     /**
223      * A scope for [schedulePrefetch] callbacks. The scope provides additional information about a
224      * prefetched item.
225      */
226     interface LazyLayoutPrefetchResultScope {
227 
228         /** The amount of placeables composed into this item. */
229         val placeablesCount: Int
230 
231         /** The index of the prefetched item. */
232         val index: Int
233 
234         /** Retrieves the latest measured size for a given placeable [placeableIndex] in pixels. */
235         fun getSize(placeableIndex: Int): IntSize
236     }
237 
238     private inner class NestedPrefetchScopeImpl(override val nestedPrefetchItemCount: Int) :
239         NestedPrefetchScope {
240 
241         val requests: List<PrefetchRequest>
242             get() = _requests
243 
244         private val _requests: MutableList<PrefetchRequest> = mutableListOf()
245 
246         override fun schedulePrecomposition(index: Int) {
247             val prefetchHandleProvider = prefetchHandleProvider ?: return
248             _requests.add(
249                 prefetchHandleProvider.createNestedPrefetchRequest(index, prefetchMetrics)
250             )
251         }
252 
253         override fun schedulePrecompositionAndPremeasure(index: Int, constraints: Constraints) {
254             val prefetchHandleProvider = prefetchHandleProvider ?: return
255             _requests.add(
256                 prefetchHandleProvider.createNestedPrefetchRequest(
257                     index,
258                     constraints,
259                     prefetchMetrics
260                 )
261             )
262         }
263     }
264 }
265 
266 internal const val UnspecifiedNestedPrefetchCount = -1
267 
268 /**
269  * A scope which allows nested prefetches to be requested for the precomposition of a LazyLayout.
270  */
271 @ExperimentalFoundationApi
272 sealed interface NestedPrefetchScope {
273 
274     /**
275      * The projected number of nested items that should be prefetched during a Nested Prefetching of
276      * an internal LazyLayout. This will return -1 if a projection isn't available yet. The parent
277      * Lazy Layout will use information about an item's content type and number of visible items to
278      * calculate the necessary number of items that a child layout will need to prefetch.
279      */
280     val nestedPrefetchItemCount: Int
281         get() = UnspecifiedNestedPrefetchCount
282 
283     /**
284      * Requests a child index to be prefetched as part of the prefetch of a parent LazyLayout.
285      *
286      * The prefetch will only do the precomposition for the new item. If you also want to premeasure
287      * please use a second overload accepting a [Constraints] param.
288      *
289      * @param index item index to prefetch.
290      */
291     @Deprecated(
292         "Please use schedulePrecomposition(index) instead",
293         level = DeprecationLevel.WARNING
294     )
schedulePrefetchnull295     fun schedulePrefetch(index: Int) = schedulePrecomposition(index)
296 
297     /**
298      * Requests a child index to be precomposed as part of the prefetch of a parent LazyLayout.
299      *
300      * The prefetch will only do the precomposition for the new item. If you also want to premeasure
301      * please use [schedulePrecompositionAndPremeasure].
302      *
303      * @param index item index to prefetch.
304      */
305     fun schedulePrecomposition(index: Int)
306 
307     /**
308      * Requests a child index to be prefetched as part of the prefetch of a parent LazyLayout.
309      *
310      * @param index the index of the child to prefetch.
311      * @param constraints [Constraints] to use for premeasuring.
312      */
313     @Deprecated(
314         "Please use schedulePremeasure(index, constraints) instead",
315         level = DeprecationLevel.WARNING
316     )
317     fun schedulePrefetch(index: Int, constraints: Constraints) =
318         schedulePrecompositionAndPremeasure(index, constraints)
319 
320     /**
321      * Requests a child index to be precomposed and premeasured as part of the prefetch of a parent
322      * LazyLayout. If you just want to precompose an item use [schedulePrecomposition] instead.
323      *
324      * @param index the index of the child to prefetch.
325      * @param constraints [Constraints] to use for premeasuring.
326      */
327     fun schedulePrecompositionAndPremeasure(index: Int, constraints: Constraints)
328 }
329 
330 /**
331  * [PrefetchMetrics] tracks timings for subcompositions so that they can be used to estimate whether
332  * we can fit prefetch work into idle time without delaying the start of the next frame.
333  */
334 internal class PrefetchMetrics {
335 
336     /**
337      * We keep the overall average numbers and averages for each content type separately. the idea
338      * is once we encounter a new content type we don't want to start with no averages, instead we
339      * use the overall averages initially until we collected more data.
340      */
341     fun getAverage(contentType: Any?): Averages {
342         val lastUsedAverage = this@PrefetchMetrics.lastUsedAverage
343         return if (lastUsedContentType === contentType && lastUsedAverage != null) {
344             lastUsedAverage
345         } else {
346             averagesByContentType
347                 .getOrPut(contentType) { Averages() }
348                 .also {
349                     this.lastUsedContentType = contentType
350                     this.lastUsedAverage = it
351                 }
352         }
353     }
354 
355     private val averagesByContentType = mutableScatterMapOf<Any?, Averages>()
356 
357     private var lastUsedContentType: Any? = null
358     private var lastUsedAverage: Averages? = null
359 }
360 
361 internal class Averages {
362     /** Average time the full composition phase has taken. */
363     var compositionTimeNanos: Long = 0L
364     /** Average time needed to resume the pausable composition until the next interruption. */
365     var resumeTimeNanos: Long = 0L
366     /** Average time needed to pause the pausable composition. */
367     var pauseTimeNanos: Long = 0L
368     /** Average time the apply phase has taken. */
369     var applyTimeNanos: Long = 0L
370     /** Average time the measure phase has taken. */
371     var measureTimeNanos: Long = 0L
372     /** Average number of nested prefetch items. */
373     var nestedPrefetchCount: Int = UnspecifiedNestedPrefetchCount
374 
saveCompositionTimeNanosnull375     fun saveCompositionTimeNanos(timeNanos: Long) {
376         compositionTimeNanos = calculateAverageTime(timeNanos, compositionTimeNanos)
377     }
378 
saveResumeTimeNanosnull379     fun saveResumeTimeNanos(timeNanos: Long) {
380         resumeTimeNanos = calculateAverageTime(timeNanos, resumeTimeNanos)
381     }
382 
savePauseTimeNanosnull383     fun savePauseTimeNanos(timeNanos: Long) {
384         pauseTimeNanos = calculateAverageTime(timeNanos, pauseTimeNanos)
385     }
386 
saveApplyTimeNanosnull387     fun saveApplyTimeNanos(timeNanos: Long) {
388         applyTimeNanos = calculateAverageTime(timeNanos, applyTimeNanos)
389     }
390 
saveMeasureTimeNanosnull391     fun saveMeasureTimeNanos(timeNanos: Long) {
392         measureTimeNanos = calculateAverageTime(timeNanos, measureTimeNanos)
393     }
394 
saveNestedPrefetchCountnull395     fun saveNestedPrefetchCount(count: Int) {
396         nestedPrefetchCount = calculateAverageCount(count, nestedPrefetchCount)
397     }
398 
calculateAverageTimenull399     private fun calculateAverageTime(new: Long, current: Long): Long {
400         // Calculate a weighted moving average of time taken to compose an item. We use weighted
401         // moving average to bias toward more recent measurements, and to minimize storage /
402         // computation cost. (the idea is taken from RecycledViewPool)
403         return if (current == 0L) {
404             new
405         } else {
406             // dividing first to avoid a potential overflow
407             current / 4 * 3 + new / 4
408         }
409     }
410 
calculateAverageCountnull411     private fun calculateAverageCount(new: Int, current: Int): Int {
412         return if (current == UnspecifiedNestedPrefetchCount) {
413             new
414         } else {
415             (current * 3 + new) / 4
416         }
417     }
418 
clearMeasureTimenull419     fun clearMeasureTime() {
420         measureTimeNanos = 0L
421     }
422 }
423 
424 @ExperimentalFoundationApi
425 private object DummyHandle : PrefetchHandle {
cancelnull426     override fun cancel() {}
427 
markAsUrgentnull428     override fun markAsUrgent() {}
429 }
430 
431 /**
432  * PrefetchHandleProvider is used to connect the [LazyLayoutPrefetchState], which provides the API
433  * to schedule prefetches, to a [LazyLayoutItemContentFactory] which resolves key and content from
434  * an index, [SubcomposeLayoutState] which knows how to precompose/premeasure, and a specific
435  * [PrefetchScheduler] used to execute a request.
436  */
437 @ExperimentalFoundationApi
438 internal class PrefetchHandleProvider(
439     private val itemContentFactory: LazyLayoutItemContentFactory,
440     private val subcomposeLayoutState: SubcomposeLayoutState,
441     private val executor: PrefetchScheduler
442 ) {
443     // cleared during onDisposed.
444     private var isStateActive: Boolean = true
445 
446     // when true we will pause the request with "has more work to do" before doing premeasure
447     // if we performed precomposed within the same execution.
448     @VisibleForTesting internal var shouldPauseBetweenPrecompositionAndPremeasure = false
449 
schedulePrecompositionnull450     fun schedulePrecomposition(
451         index: Int,
452         isHighPriority: Boolean,
453         prefetchMetrics: PrefetchMetrics,
454     ): PrefetchHandle =
455         HandleAndRequestImpl(index, prefetchMetrics, executor as? PriorityPrefetchScheduler, null)
456             .also {
457                 executor.executeWithPriority(it, isHighPriority)
458                 traceValue("compose:lazy:schedule_prefetch:index", index.toLong())
459             }
460 
onDisposednull461     fun onDisposed() {
462         isStateActive = false
463     }
464 
schedulePremeasurenull465     fun schedulePremeasure(
466         index: Int,
467         constraints: Constraints,
468         prefetchMetrics: PrefetchMetrics,
469         isHighPriority: Boolean,
470         onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)?
471     ): PrefetchHandle =
472         HandleAndRequestImpl(
473                 index,
474                 constraints,
475                 prefetchMetrics,
476                 executor as? PriorityPrefetchScheduler,
477                 onItemPremeasured
478             )
479             .also {
480                 executor.executeWithPriority(it, isHighPriority)
481                 traceValue("compose:lazy:schedule_prefetch:index", index.toLong())
482             }
483 
PrefetchSchedulernull484     fun PrefetchScheduler.executeWithPriority(request: PrefetchRequest, isHighPriority: Boolean) {
485         if (this is PriorityPrefetchScheduler) {
486             if (isHighPriority) {
487                 scheduleHighPriorityPrefetch(request)
488             } else {
489                 scheduleLowPriorityPrefetch(request)
490             }
491         } else {
492             schedulePrefetch(request)
493         }
494     }
495 
createNestedPrefetchRequestnull496     fun createNestedPrefetchRequest(
497         index: Int,
498         constraints: Constraints,
499         prefetchMetrics: PrefetchMetrics,
500     ): PrefetchRequest =
501         HandleAndRequestImpl(
502             index,
503             constraints = constraints,
504             prefetchMetrics,
505             executor as? PriorityPrefetchScheduler,
506             null
507         )
508 
509     fun createNestedPrefetchRequest(
510         index: Int,
511         prefetchMetrics: PrefetchMetrics,
512     ): PrefetchRequest =
513         HandleAndRequestImpl(index, prefetchMetrics, executor as? PriorityPrefetchScheduler, null)
514 
515     @ExperimentalFoundationApi
516     private inner class HandleAndRequestImpl(
517         override val index: Int,
518         private val prefetchMetrics: PrefetchMetrics,
519         private val priorityPrefetchScheduler: PriorityPrefetchScheduler?,
520         private val onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)?,
521     ) : PrefetchHandle, PrefetchRequest, LazyLayoutPrefetchResultScope {
522 
523         constructor(
524             index: Int,
525             constraints: Constraints,
526             prefetchMetrics: PrefetchMetrics,
527             priorityPrefetchScheduler: PriorityPrefetchScheduler?,
528             onItemPremeasured: (LazyLayoutPrefetchResultScope.() -> Unit)?
529         ) : this(index, prefetchMetrics, priorityPrefetchScheduler, onItemPremeasured) {
530             premeasureConstraints = constraints
531         }
532 
533         private var premeasureConstraints: Constraints? = null
534         private var precomposeHandle: SubcomposeLayoutState.PrecomposedSlotHandle? = null
535         private var pausedPrecomposition: SubcomposeLayoutState.PausedPrecomposition? = null
536         private var isMeasured = false
537         private var isCanceled = false
538         private var isApplied = false
539         private var keyUsedForComposition: Any? = null
540 
541         private var hasResolvedNestedPrefetches = false
542         private var nestedPrefetchController: NestedPrefetchController? = null
543         private var isUrgent = false
544 
545         private val isComposed
546             get() = isApplied || pausedPrecomposition?.isComplete == true
547 
548         override fun cancel() {
549             if (!isCanceled) {
550                 isCanceled = true
551                 cleanUp()
552             }
553         }
554 
555         override fun markAsUrgent() {
556             isUrgent = true
557         }
558 
559         override val placeablesCount: Int
560             get() = (precomposeHandle?.placeablesCount ?: 0)
561 
562         override fun getSize(placeableIndex: Int): IntSize {
563             return (precomposeHandle?.getSize(placeableIndex) ?: IntSize.Zero)
564         }
565 
566         private fun PrefetchRequestScope.shouldExecute(available: Long, average: Long): Boolean {
567             // Each step execution is prioritized as follows:
568             // 1) If it is urgent, we always execute if we have time in the frame.
569             // 2) If we're in idle mode, we always execute if we have time in the frame.
570             // 3) In regular circumstances, we look at the average time this step took and execute
571             // only if we have time.
572             val required =
573                 if (isUrgent || (priorityPrefetchScheduler?.isFrameIdle ?: false)) 0 else average
574             return available > required
575         }
576 
577         private var availableTimeNanos = 0L
578         private var elapsedTimeNanos = 0L
579         private var startTime = markNow()
580 
581         private fun resetAvailableTimeTo(availableTimeNanos: Long) {
582             this.availableTimeNanos = availableTimeNanos
583             startTime = markNow()
584             elapsedTimeNanos = 0L
585             traceValue("compose:lazy:prefetch:available_time_nanos", availableTimeNanos)
586         }
587 
588         private fun updateElapsedAndAvailableTime() {
589             val now = markNow()
590             elapsedTimeNanos = (now - startTime).inWholeNanoseconds
591             availableTimeNanos -= elapsedTimeNanos
592             startTime = now
593             traceValue("compose:lazy:prefetch:available_time_nanos", availableTimeNanos)
594         }
595 
596         override fun PrefetchRequestScope.execute(): Boolean {
597             // check if the state that generated this request is still active.
598             if (!isStateActive) return false
599             return if (isUrgent) {
600                     trace("compose:lazy:prefetch:execute:urgent") { executeRequest() }
601                 } else {
602                     executeRequest()
603                 }
604                 .also {
605                     // execution for this item finished, reset the trace value
606                     traceValue("compose:lazy:prefetch:execute:item", -1)
607                 }
608         }
609 
610         private fun cleanUp() {
611             pausedPrecomposition?.cancel()
612             pausedPrecomposition = null
613             precomposeHandle?.dispose()
614             precomposeHandle = null
615             nestedPrefetchController = null
616         }
617 
618         private fun PrefetchRequestScope.executeRequest(): Boolean {
619             traceValue("compose:lazy:prefetch:execute:item", index.toLong())
620             val itemProvider = itemContentFactory.itemProvider()
621             val isValid = !isCanceled && index in 0 until itemProvider.itemCount
622             if (!isValid) {
623                 cleanUp()
624                 return false
625             }
626 
627             val key = itemProvider.getKey(index)
628             if (keyUsedForComposition != null && key != keyUsedForComposition) {
629                 // key for the requested index changed, the request is now invalid
630                 cleanUp()
631                 return false
632             }
633 
634             val contentType = itemProvider.getContentType(index)
635             val average = prefetchMetrics.getAverage(contentType)
636             val wasComposedAtStart = isComposed
637 
638             // we save the value we get from availableTimeNanos() into a local variable once
639             // and manually update it later by calling updateElapsedAndAvailableTime()
640             resetAvailableTimeTo(availableTimeNanos())
641             if (!isComposed) {
642                 if (ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled) {
643                     if (
644                         shouldExecute(
645                             availableTimeNanos,
646                             average.resumeTimeNanos + average.pauseTimeNanos
647                         )
648                     ) {
649                         trace("compose:lazy:prefetch:compose") {
650                             performPausableComposition(key, contentType, average)
651                         }
652                     }
653                 } else {
654                     if (shouldExecute(availableTimeNanos, average.compositionTimeNanos)) {
655                         trace("compose:lazy:prefetch:compose") {
656                             performFullComposition(key, contentType)
657                         }
658                         updateElapsedAndAvailableTime()
659                         average.saveCompositionTimeNanos(elapsedTimeNanos)
660                     }
661                 }
662                 if (!isComposed) {
663                     return true
664                 }
665             }
666 
667             if (pausedPrecomposition != null) {
668                 if (shouldExecute(availableTimeNanos, average.applyTimeNanos)) {
669                     trace("compose:lazy:prefetch:apply") { performApply() }
670                     updateElapsedAndAvailableTime()
671                     average.saveApplyTimeNanos(elapsedTimeNanos)
672                 } else {
673                     return true
674                 }
675             }
676 
677             // Nested prefetch logic is best-effort: if nested LazyLayout children are
678             // added/removed/updated after we've resolved nested prefetch states here or
679             // resolved nestedPrefetchRequests below, those changes won't be taken into account.
680             if (!hasResolvedNestedPrefetches) {
681                 if (availableTimeNanos > 0) {
682                     trace("compose:lazy:prefetch:resolve-nested") {
683                         nestedPrefetchController = resolveNestedPrefetchStates()
684                         hasResolvedNestedPrefetches = true
685                     }
686                 } else {
687                     return true
688                 }
689             }
690             val hasMoreWork =
691                 nestedPrefetchController?.run {
692                     executeNestedPrefetches(average.nestedPrefetchCount, isUrgent)
693                 } ?: false
694             if (hasMoreWork) {
695                 return true
696             }
697 
698             // only update the time and traces if we actually executed a nested prefetch request
699             if (nestedPrefetchController?.executedNestedPrefetch == true) {
700                 updateElapsedAndAvailableTime()
701                 // set the item value again since it will have changed in the nested block.
702                 traceValue("compose:lazy:prefetch:execute:item", index.toLong())
703                 // re-enable it next time we execute a nested prefetch request
704                 nestedPrefetchController?.executedNestedPrefetch = false
705             }
706 
707             val constraints = premeasureConstraints
708             if (!isMeasured && constraints != null) {
709                 if (shouldPauseBetweenPrecompositionAndPremeasure && !wasComposedAtStart) {
710                     return true
711                 }
712                 if (shouldExecute(availableTimeNanos, average.measureTimeNanos)) {
713                     trace("compose:lazy:prefetch:measure") { performMeasure(constraints) }
714                     updateElapsedAndAvailableTime()
715                     average.saveMeasureTimeNanos(elapsedTimeNanos)
716                     onItemPremeasured?.invoke(this@HandleAndRequestImpl)
717                 } else {
718                     return true
719                 }
720             }
721 
722             // once we've measured this item we now have the up to date "ideal" number of
723             // nested prefetches we'd like to perform, save that to the average.
724             val controller = nestedPrefetchController
725             if (
726                 isAutomaticNestedPrefetchEnabled &&
727                     isMeasured &&
728                     hasResolvedNestedPrefetches &&
729                     controller != null
730             ) {
731                 val idealNestedPrefetchCount = controller.collectIdealNestedPrefetchCount()
732                 average.saveNestedPrefetchCount(idealNestedPrefetchCount)
733                 val lastNumberOfNestedPrefetchItems = controller.collectNestedPrefetchedItemsCount()
734                 // if in the last pass we nested prefetched less items than we will in the next
735                 // pass,
736                 // this means our measure time for this item will be wrong, let's reset it and
737                 // collect it again the next time.
738                 if (lastNumberOfNestedPrefetchItems < idealNestedPrefetchCount) {
739                     average.clearMeasureTime()
740                 }
741             }
742 
743             // All our work is done.
744             return false
745         }
746 
747         private var pauseRequested = false
748 
749         private fun PrefetchRequestScope.performPausableComposition(
750             key: Any,
751             contentType: Any?,
752             averages: Averages
753         ) {
754             val composition =
755                 pausedPrecomposition
756                     ?: run {
757                         val content = itemContentFactory.getContent(index, key, contentType)
758                         subcomposeLayoutState.createPausedPrecomposition(key, content).also {
759                             pausedPrecomposition = it
760                             keyUsedForComposition = key
761                         }
762                     }
763 
764             pauseRequested = false
765 
766             while (!composition.isComplete && !pauseRequested) {
767                 composition.resume {
768                     if (!pauseRequested) {
769                         updateElapsedAndAvailableTime()
770                         averages.saveResumeTimeNanos(elapsedTimeNanos)
771                         pauseRequested =
772                             !shouldExecute(
773                                 availableTimeNanos,
774                                 averages.resumeTimeNanos + averages.pauseTimeNanos
775                             )
776                     }
777                     pauseRequested
778                 }
779             }
780 
781             updateElapsedAndAvailableTime()
782             if (pauseRequested) {
783                 averages.savePauseTimeNanos(elapsedTimeNanos)
784             } else {
785                 averages.saveResumeTimeNanos(elapsedTimeNanos)
786             }
787         }
788 
789         private fun performFullComposition(key: Any, contentType: Any?) {
790             requirePrecondition(precomposeHandle == null) { "Request was already composed!" }
791             val content = itemContentFactory.getContent(index, key, contentType)
792             keyUsedForComposition = key
793             precomposeHandle = subcomposeLayoutState.precompose(key, content)
794             isApplied = true
795         }
796 
797         private fun performApply() {
798             val precomposition = requireNotNull(pausedPrecomposition) { "Nothing to apply!" }
799             precomposeHandle = precomposition.apply()
800             pausedPrecomposition = null
801             isApplied = true
802         }
803 
804         private fun performMeasure(constraints: Constraints) {
805             requirePrecondition(!isCanceled) {
806                 "Callers should check whether the request is still valid before calling " +
807                     "performMeasure()"
808             }
809             requirePrecondition(!isMeasured) { "Request was already measured!" }
810             isMeasured = true
811             val handle =
812                 requirePreconditionNotNull(precomposeHandle) {
813                     "performComposition() must be called before performMeasure()"
814                 }
815             repeat(handle.placeablesCount) { placeableIndex ->
816                 handle.premeasure(placeableIndex, constraints)
817             }
818         }
819 
820         private fun resolveNestedPrefetchStates(): NestedPrefetchController? {
821             val precomposedSlotHandle =
822                 requirePreconditionNotNull(precomposeHandle) {
823                     "Should precompose before resolving nested prefetch states"
824                 }
825 
826             var nestedStates: MutableList<LazyLayoutPrefetchState>? = null
827             precomposedSlotHandle.traverseDescendants(TraversablePrefetchStateNodeKey) {
828                 val prefetchState = (it as TraversablePrefetchStateNode).prefetchState
829                 nestedStates =
830                     nestedStates?.apply { add(prefetchState) } ?: mutableListOf(prefetchState)
831                 TraverseDescendantsAction.SkipSubtreeAndContinueTraversal
832             }
833             return nestedStates?.let { NestedPrefetchController(it) }
834         }
835 
836         override fun toString(): String =
837             "HandleAndRequestImpl { index = $index, constraints = $premeasureConstraints, " +
838                 "isComposed = $isComposed, isMeasured = $isMeasured, isCanceled = $isCanceled }"
839 
840         private inner class NestedPrefetchController(
841             private val states: List<LazyLayoutPrefetchState>
842         ) {
843 
844             // This array is parallel to nestedPrefetchStates, so index 0 in nestedPrefetchStates
845             // corresponds to index 0 in this array, etc.
846             private val requestsByState: Array<List<PrefetchRequest>?> = arrayOfNulls(states.size)
847             private var stateIndex: Int = 0
848             private var requestIndex: Int = 0
849             var executedNestedPrefetch: Boolean = false
850 
851             init {
852                 requirePrecondition(states.isNotEmpty()) {
853                     "NestedPrefetchController shouldn't be created with no states"
854                 }
855             }
856 
857             fun PrefetchRequestScope.executeNestedPrefetches(
858                 nestedPrefetchCount: Int,
859                 isUrgent: Boolean
860             ): Boolean {
861                 if (stateIndex >= states.size) {
862                     return false
863                 }
864                 checkPrecondition(!isCanceled) {
865                     "Should not execute nested prefetch on canceled request"
866                 }
867 
868                 // If we have automatic nested prefetch enabled, it means we can update the
869                 // nested prefetch count for some of the layouts in this item.
870                 if (isAutomaticNestedPrefetchEnabled) {
871                     trace("compose:lazy:prefetch:update_nested_prefetch_count") {
872                         states.fastForEach { it.realizedNestedPrefetchCount = nestedPrefetchCount }
873                     }
874                 }
875 
876                 trace("compose:lazy:prefetch:nested") {
877                     while (stateIndex < states.size) {
878                         if (requestsByState[stateIndex] == null) {
879                             if (availableTimeNanos() <= 0) {
880                                 // When we have time again, we'll resolve nested requests for this
881                                 // state
882                                 return true
883                             }
884 
885                             requestsByState[stateIndex] =
886                                 states[stateIndex].collectNestedPrefetchRequests()
887                         }
888 
889                         val nestedRequests = requestsByState[stateIndex]!!
890                         while (requestIndex < nestedRequests.size) {
891                             val hasMoreWork =
892                                 with(nestedRequests[requestIndex]) {
893                                     // mark this nested request as urgent, because its parent
894                                     // request is
895                                     // urgent
896                                     if (isUrgent) {
897                                         (this as? HandleAndRequestImpl)?.markAsUrgent()
898                                     }
899                                     executedNestedPrefetch = true
900                                     execute()
901                                 }
902                             if (hasMoreWork) {
903                                 return true
904                             } else {
905                                 requestIndex++
906                             }
907                         }
908 
909                         requestIndex = 0
910                         stateIndex++
911                     }
912                 }
913 
914                 return false
915             }
916 
917             fun collectIdealNestedPrefetchCount(): Int {
918                 var count = Int.MAX_VALUE
919                 states.fastForEach {
920                     // use the minimum ideal counts provided by all nested layouts in this item.
921                     count = minOf(count, it.idealNestedPrefetchCount)
922                 }
923                 return if (count == Int.MAX_VALUE) 0 else count
924             }
925 
926             fun collectNestedPrefetchedItemsCount(): Int {
927                 var count = Int.MAX_VALUE
928                 states.fastForEach {
929                     // use the minimum ideal counts provided by all nested layouts in this item.
930                     count = minOf(count, it.lastNumberOfNestedPrefetchItems)
931                 }
932                 return if (count == Int.MAX_VALUE) 0 else count
933             }
934         }
935     }
936 }
937 
938 private const val TraversablePrefetchStateNodeKey =
939     "androidx.compose.foundation.lazy.layout.TraversablePrefetchStateNode"
940 
941 /**
942  * A modifier which lets the [LazyLayoutPrefetchState] for a [LazyLayout] to be discoverable via
943  * [TraversableNode] traversal.
944  */
945 @ExperimentalFoundationApi
traversablePrefetchStatenull946 internal fun Modifier.traversablePrefetchState(
947     lazyLayoutPrefetchState: LazyLayoutPrefetchState?
948 ): Modifier {
949     return lazyLayoutPrefetchState?.let { this then TraversablePrefetchStateModifierElement(it) }
950         ?: this
951 }
952 
953 @ExperimentalFoundationApi
954 private class TraversablePrefetchStateNode(
955     var prefetchState: LazyLayoutPrefetchState,
956 ) : Modifier.Node(), TraversableNode {
957 
958     override val traverseKey: String = TraversablePrefetchStateNodeKey
959 }
960 
961 @ExperimentalFoundationApi
962 private data class TraversablePrefetchStateModifierElement(
963     private val prefetchState: LazyLayoutPrefetchState,
964 ) : ModifierNodeElement<TraversablePrefetchStateNode>() {
createnull965     override fun create() = TraversablePrefetchStateNode(prefetchState)
966 
967     override fun update(node: TraversablePrefetchStateNode) {
968         node.prefetchState = prefetchState
969     }
970 
inspectablePropertiesnull971     override fun InspectorInfo.inspectableProperties() {
972         name = "traversablePrefetchState"
973         value = prefetchState
974     }
975 }
976 
977 private val ZeroConstraints = Constraints(maxWidth = 0, maxHeight = 0)
978