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 ToleranceRange { 17 leftToleranceEdge: number; 18 rightToleranceEdge: number; 19 prefetchCountMinRatioLeft: number; 20 prefetchCountMaxRatioLeft: number; 21 prefetchCountMinRatioRight: number; 22 prefetchCountMaxRatioRight: number; 23} 24 25type PrefetchCountMaxChangedCallback = () => void; 26 27type UpdateResult = 'ratio-changed' | 'ratio-not-changed'; 28 29// eslint-disable-next-line @typescript-eslint/no-unused-vars 30class PrefetchRangeRatio { 31 private readonly TOLERANCE_RANGES: ToleranceRange[] = [ 32 { 33 leftToleranceEdge: 140, 34 rightToleranceEdge: 290, 35 prefetchCountMinRatioLeft: 0.5, 36 prefetchCountMaxRatioLeft: 0.5, 37 prefetchCountMinRatioRight: 0.25, 38 prefetchCountMaxRatioRight: 1, 39 }, 40 { 41 leftToleranceEdge: 3000, 42 rightToleranceEdge: 4000, 43 prefetchCountMinRatioLeft: 0.25, 44 prefetchCountMaxRatioLeft: 1, 45 prefetchCountMinRatioRight: 0.25, 46 prefetchCountMaxRatioRight: 0.25, 47 }, 48 ]; 49 private readonly ACTIVE_DEGREE: number = 0; 50 private readonly VISIBLE_DEGREE: number = 2.5; 51 private meanPrefetchTime = 0; 52 private leftToleranceEdge = Number.MIN_VALUE; 53 private rightToleranceEdge = 250; 54 55 constructor( 56 private readonly itemsOnScreen: ItemsOnScreenProvider, 57 private readonly fetchedRegistry: FetchedRegistry, 58 private readonly fetchingRegistry: FetchingRegistry, 59 private readonly logger: ILogger = dummyLogger, 60 ) {} 61 62 private callbacks: PrefetchCountMaxChangedCallback[] = []; 63 64 register(callback: PrefetchCountMaxChangedCallback): void { 65 this.callbacks.push(callback); 66 } 67 68 private rangeInternal = RatioRange.newEmpty(); 69 70 get range(): RatioRange { 71 return this.rangeInternal; 72 } 73 74 setEmptyRange(): void { 75 this.rangeInternal = RatioRange.newEmpty(); 76 } 77 78 private minRatioInternal = 0.25 * 0.6; 79 private maxRatioInternal = 0.5; 80 81 get maxRatio(): number { 82 return this.maxRatioInternal; 83 } 84 85 get minRatio(): number { 86 return this.minRatioInternal; 87 } 88 89 private hysteresisEnabledInternal = false; 90 91 get hysteresisEnabled(): boolean { 92 return this.hysteresisEnabledInternal; 93 } 94 95 set hysteresisEnabled(value: boolean) { 96 this.hysteresisEnabledInternal = value; 97 } 98 99 private oldRatioInternal = 0; 100 101 set oldRatio(ratio: number) { 102 this.oldRatioInternal = ratio; 103 } 104 105 get oldRatio(): number { 106 return this.oldRatioInternal; 107 } 108 109 private updateTiming(index: number, prefetchDuration: number): void { 110 const weight = 0.95; 111 const localPrefetchDuration = 20; 112 113 let isFetchLocal = prefetchDuration < localPrefetchDuration; 114 let isFetchLatecomer = this.fetchingRegistry.isFetchLatecomer(index, this.itemsOnScreen.meanValue); 115 116 if (!isFetchLocal && !isFetchLatecomer) { 117 this.meanPrefetchTime = this.meanPrefetchTime * weight + (1 - weight) * prefetchDuration; 118 } 119 120 this.logger.debug( 121 `prefetchDifference prefetchDur=${prefetchDuration}, meanPrefetchDur=${this.meanPrefetchTime}, ` + 122 `isFetchLocal=${isFetchLocal}, isFetchLatecomer=${isFetchLatecomer}`, 123 ); 124 } 125 126 update(index: number, prefetchDuration: number): UpdateResult { 127 this.updateTiming(index, prefetchDuration); 128 129 if (this.meanPrefetchTime >= this.leftToleranceEdge && this.meanPrefetchTime <= this.rightToleranceEdge) { 130 return 'ratio-not-changed'; 131 } 132 133 let ratioChanged = false; 134 135 if (this.meanPrefetchTime > this.rightToleranceEdge) { 136 ratioChanged = this.updateOnGreaterThanRight(); 137 } else if (this.meanPrefetchTime < this.leftToleranceEdge) { 138 ratioChanged = this.updateOnLessThanLeft(); 139 } 140 141 if (ratioChanged) { 142 this.notifyObservers(); 143 } 144 145 return ratioChanged ? 'ratio-changed' : 'ratio-not-changed'; 146 } 147 148 private updateOnLessThanLeft(): boolean { 149 let ratioChanged = false; 150 for (let i = this.TOLERANCE_RANGES.length - 1; i >= 0; i--) { 151 const limit = this.TOLERANCE_RANGES[i]; 152 if (this.meanPrefetchTime < limit.leftToleranceEdge) { 153 ratioChanged = true; 154 this.maxRatioInternal = limit.prefetchCountMaxRatioLeft; 155 this.minRatioInternal = limit.prefetchCountMinRatioLeft; 156 this.rightToleranceEdge = limit.rightToleranceEdge; 157 if (i !== 0) { 158 this.leftToleranceEdge = this.TOLERANCE_RANGES[i - 1].leftToleranceEdge; 159 } else { 160 this.leftToleranceEdge = Number.MIN_VALUE; 161 } 162 } 163 } 164 return ratioChanged; 165 } 166 167 private updateOnGreaterThanRight(): boolean { 168 let ratioChanged = false; 169 for (let i = 0; i < this.TOLERANCE_RANGES.length; i++) { 170 const limit = this.TOLERANCE_RANGES[i]; 171 if (this.meanPrefetchTime > limit.rightToleranceEdge) { 172 ratioChanged = true; 173 this.maxRatioInternal = limit.prefetchCountMaxRatioRight; 174 this.minRatioInternal = limit.prefetchCountMinRatioRight; 175 this.leftToleranceEdge = limit.leftToleranceEdge; 176 if (i + 1 !== this.TOLERANCE_RANGES.length) { 177 this.rightToleranceEdge = this.TOLERANCE_RANGES[i + 1].rightToleranceEdge; 178 } else { 179 this.rightToleranceEdge = Number.MAX_VALUE; 180 } 181 } 182 } 183 return ratioChanged; 184 } 185 186 calculateRatio(prefetchCount: number, totalCount: number): number { 187 const visibleRange = this.itemsOnScreen.visibleRange; 188 189 let start: number = 0; 190 let end: number = 0; 191 192 switch (this.itemsOnScreen.direction) { 193 case 'UNKNOWN': 194 start = Math.max(0, visibleRange.start - prefetchCount); 195 end = Math.min(totalCount, visibleRange.end + prefetchCount); 196 break; 197 case 'UP': 198 start = Math.max(0, visibleRange.start - prefetchCount); 199 end = Math.min(totalCount, visibleRange.end + Math.round(0.5 * prefetchCount)); 200 break; 201 case 'DOWN': 202 start = Math.max(0, visibleRange.start - Math.round(0.5 * prefetchCount)); 203 end = Math.min(totalCount, visibleRange.end + prefetchCount); 204 break; 205 } 206 207 const evaluatedPrefetchRange = new IndexRange(start, end); 208 const completedActive = this.fetchedRegistry.getFetchedInRange(evaluatedPrefetchRange); 209 const completedVisible = this.fetchedRegistry.getFetchedInRange(visibleRange); 210 211 if (evaluatedPrefetchRange.length === 0 || visibleRange.length === 0) { 212 return 0; 213 } 214 215 this.logger.debug(`active_degree=${this.ACTIVE_DEGREE}, visible_degree=${this.VISIBLE_DEGREE}`); 216 this.logger.debug( 217 `evaluatedPrefetchRange=${evaluatedPrefetchRange}, visibleRange=${visibleRange}, active_ratio=${Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE)}, visible_ratio=${Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE)}, completedActive=${completedActive}, evaluatedPrefetchRange.length=${evaluatedPrefetchRange.length}, visibleRange.length=${visibleRange.length}`, 218 ); 219 220 const ratio = 221 Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE) * 222 Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE); 223 224 this.logger.debug( 225 `calculateRatio ratio=${ratio}, completedActive=${completedActive}, evaluatedPrefetchRange.length=${evaluatedPrefetchRange.length}, ` + 226 `completedVisible=${completedVisible}, visibleRange.length=${visibleRange.length}`, 227 ); 228 229 return Math.min(1, ratio); 230 } 231 232 updateRatioRange(ratio: number): void { 233 if (ratio > this.oldRatioInternal) { 234 this.rangeInternal = new RatioRange(new RangeEdge(this.oldRatioInternal, false), new RangeEdge(ratio, true)); 235 } else { 236 this.rangeInternal = new RatioRange(new RangeEdge(ratio, true), new RangeEdge(this.oldRatioInternal, false)); 237 } 238 this.oldRatioInternal = ratio; 239 } 240 241 private notifyObservers(): void { 242 this.callbacks.forEach((callback) => callback()); 243 } 244} 245