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