1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16interface IFetchingRangeEvaluator { 17 updateRangeToFetch(whatHappened: RangeUpdateEvent): void; 18} 19 20type RangeUpdateEvent = 21 | { 22 kind: 'visible-area-changed'; 23 minVisible: number; 24 maxVisible: number; 25 } 26 | { 27 kind: 'item-fetched'; 28 itemIndex: number; 29 fetchDuration: number; 30 } 31 | { 32 kind: 'collection-changed'; 33 totalCount: number; 34 } 35 | { 36 kind: 'item-added'; 37 itemIndex: number; 38 } 39 | { 40 kind: 'item-removed'; 41 itemIndex: number; 42 }; 43 44// eslint-disable-next-line @typescript-eslint/no-unused-vars 45class FetchingRangeEvaluator implements IFetchingRangeEvaluator { 46 protected totalItems = 0; 47 48 constructor( 49 private readonly itemsOnScreen: ItemsOnScreenProvider, 50 private readonly prefetchCount: PrefetchCount, 51 private readonly prefetchRangeRatio: PrefetchRangeRatio, 52 protected readonly fetchedRegistry: FetchedRegistry, 53 private readonly logger: ILogger = dummyLogger, 54 ) {} 55 56 updateRangeToFetch(whatHappened: RangeUpdateEvent): void { 57 switch (whatHappened.kind) { 58 case 'visible-area-changed': 59 this.onVisibleAreaChange(whatHappened.minVisible, whatHappened.maxVisible); 60 break; 61 case 'item-fetched': 62 this.onItemFetched(whatHappened.itemIndex, whatHappened.fetchDuration); 63 break; 64 case 'collection-changed': 65 this.onCollectionChanged(whatHappened.totalCount); 66 break; 67 case 'item-added': 68 this.onItemAdded(whatHappened.itemIndex); 69 break; 70 case 'item-removed': 71 this.onItemDeleted(whatHappened.itemIndex); 72 break; 73 default: 74 assertNever(whatHappened); 75 } 76 } 77 78 protected onVisibleAreaChange(minVisible: number, maxVisible: number): void { 79 const oldVisibleRange = this.itemsOnScreen.visibleRange; 80 this.itemsOnScreen.update(minVisible, maxVisible); 81 82 this.logger.debug( 83 `visibleAreaChanged itemsOnScreen=${this.itemsOnScreen.visibleRange.length}, meanImagesOnScreen=${this.itemsOnScreen.meanValue}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}, prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}`, 84 ); 85 86 if (!oldVisibleRange.equals(this.itemsOnScreen.visibleRange)) { 87 this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('visible-area-changed'); 88 const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems); 89 this.fetchedRegistry.updateRangeToFetch(rangeToFetch); 90 } 91 } 92 93 protected onItemFetched(index: number, fetchDuration: number): void { 94 if (!this.fetchedRegistry.rangeToFetch.contains(index)) { 95 return; 96 } 97 98 this.logger.debug(`onItemFetched`); 99 let maxRatioChanged = false; 100 if (this.prefetchRangeRatio.update(index, fetchDuration) === 'ratio-changed') { 101 maxRatioChanged = true; 102 this.logger.debug( 103 `choosePrefetchCountLimit prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}, prefetchCountMinRatio=${this.prefetchRangeRatio.minRatio}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}`, 104 ); 105 } 106 107 this.fetchedRegistry.addFetched(index); 108 109 this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('resolved', maxRatioChanged); 110 const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems); 111 this.fetchedRegistry.updateRangeToFetch(rangeToFetch); 112 } 113 114 private evaluatePrefetchCount(event: 'resolved' | 'visible-area-changed', maxRatioChanged?: boolean): number { 115 let ratio = this.prefetchRangeRatio.calculateRatio(this.prefetchCount.prefetchCountValue, this.totalItems); 116 let evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio); 117 118 if (maxRatioChanged) { 119 ratio = this.prefetchRangeRatio.calculateRatio(evaluatedPrefetchCount, this.totalItems); 120 evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio); 121 } 122 123 if (!this.prefetchRangeRatio.hysteresisEnabled) { 124 if (event === 'resolved') { 125 this.prefetchRangeRatio.updateRatioRange(ratio); 126 this.prefetchRangeRatio.hysteresisEnabled = true; 127 } else if (event === 'visible-area-changed') { 128 this.prefetchRangeRatio.oldRatio = ratio; 129 } 130 } else if (this.prefetchRangeRatio.range.contains(ratio)) { 131 return this.prefetchCount.prefetchCountValue; 132 } else { 133 if (event === 'resolved') { 134 this.prefetchRangeRatio.updateRatioRange(ratio); 135 } else if (event === 'visible-area-changed') { 136 this.prefetchRangeRatio.setEmptyRange(); 137 this.prefetchRangeRatio.oldRatio = ratio; 138 this.prefetchRangeRatio.hysteresisEnabled = false; 139 } 140 } 141 142 this.logger.debug( 143 `evaluatePrefetchCount event=${event}, ${this.prefetchRangeRatio.hysteresisEnabled ? 'inHysteresis' : 'setHysteresis'} prefetchCount=${evaluatedPrefetchCount}, ratio=${ratio}, hysteresisRange=${this.prefetchRangeRatio.range}`, 144 ); 145 146 return evaluatedPrefetchCount; 147 } 148 149 protected onCollectionChanged(totalCount: number): void { 150 this.totalItems = Math.max(0, totalCount); 151 let newRangeToFetch = this.itemsOnScreen.visibleRange; 152 if (newRangeToFetch.end > this.totalItems) { 153 const end = this.totalItems; 154 const start = newRangeToFetch.start < end ? newRangeToFetch.start : end; 155 newRangeToFetch = new IndexRange(start, end); 156 } 157 158 this.fetchedRegistry.clearFetched(newRangeToFetch); 159 } 160 161 private onItemDeleted(itemIndex: number): void { 162 if (this.totalItems === 0) { 163 return; 164 } 165 this.totalItems--; 166 this.fetchedRegistry.removeFetched(itemIndex); 167 168 const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems); 169 this.fetchedRegistry.decrementFetchedGreaterThen(itemIndex, rangeToFetch); 170 } 171 172 private onItemAdded(itemIndex: number): void { 173 this.totalItems++; 174 if (itemIndex > this.fetchedRegistry.rangeToFetch.end) { 175 return; 176 } 177 178 const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems); 179 this.fetchedRegistry.incrementFetchedGreaterThen(itemIndex - 1, rangeToFetch); 180 } 181} 182