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: IndexRange; 152 if (this.fetchedRegistry.rangeToFetch.length > 0) { 153 newRangeToFetch = this.itemsOnScreen.visibleRange; 154 } else { 155 newRangeToFetch = this.fetchedRegistry.rangeToFetch; 156 } 157 if (newRangeToFetch.end > this.totalItems) { 158 const end = this.totalItems; 159 const start = newRangeToFetch.start < end ? newRangeToFetch.start : end; 160 newRangeToFetch = new IndexRange(start, end); 161 } 162 163 this.fetchedRegistry.clearFetched(newRangeToFetch); 164 } 165 166 private onItemDeleted(itemIndex: number): void { 167 if (this.totalItems === 0) { 168 return; 169 } 170 this.totalItems--; 171 this.fetchedRegistry.removeFetched(itemIndex); 172 173 const end = 174 this.fetchedRegistry.rangeToFetch.end < this.totalItems ? this.fetchedRegistry.rangeToFetch.end : this.totalItems; 175 const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end); 176 177 this.fetchedRegistry.decrementFetchedGreaterThen(itemIndex, rangeToFetch); 178 } 179 180 private onItemAdded(itemIndex: number): void { 181 this.totalItems++; 182 if (itemIndex > this.fetchedRegistry.rangeToFetch.end) { 183 return; 184 } 185 186 const end = this.fetchedRegistry.rangeToFetch.end + 1; 187 const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end); 188 this.fetchedRegistry.incrementFetchedGreaterThen(itemIndex - 1, rangeToFetch); 189 } 190} 191