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 16class BasicPrefetcher { 17 constructor(ds) { 18 this.prefetcher = new Prefetcher(new PrefetchRangeEvaluator(), new DefaultTimeProvider()); 19 if (ds) { 20 this.prefetcher.setDataSource(ds); 21 } 22 } 23 setDataSource(ds) { 24 this.prefetcher.setDataSource(ds); 25 } 26 visibleAreaChanged(minVisible, maxVisible) { 27 this.prefetcher.visibleAreaChanged(minVisible, maxVisible); 28 } 29} 30class FetchedRegistry { 31 constructor() { 32 this.fetchedIndicies = new Set(); 33 this.rangeToPrefetch = new IndexRange(0, 0); 34 } 35 addFetched(index) { 36 this.fetchedIndicies.add(index); 37 } 38 getFetchedInRange(range) { 39 let fetched = 0; 40 range.forEachIndex((index) => { 41 fetched += this.fetchedIndicies.has(index) ? 1 : 0; 42 }); 43 return fetched; 44 } 45 updateRangeToPrefetch(prefetchRange) { 46 this.rangeToPrefetch.subtract(prefetchRange).forEachIndex((index) => { 47 this.fetchedIndicies.delete(index); 48 }); 49 this.rangeToPrefetch = prefetchRange; 50 } 51} 52class ItemsOnScreenProvider { 53 constructor() { 54 this.firstScreen = true; 55 this.meanImagesOnScreen = 0; 56 this.minVisible = 0; 57 this.maxVisible = 0; 58 this._direction = 'UNKNOWN'; 59 this._visibleRange = new IndexRange(0, 0); 60 this.callbacks = []; 61 } 62 register(callback) { 63 this.callbacks.push(callback); 64 } 65 get visibleRange() { 66 return this._visibleRange; 67 } 68 get meanValue() { 69 return this.meanImagesOnScreen; 70 } 71 get direction() { 72 return this._direction; 73 } 74 update(minVisible, maxVisible) { 75 if (minVisible == this.minVisible && maxVisible == this.maxVisible) { 76 } 77 else if (Math.max(minVisible, this.minVisible) == minVisible && 78 Math.max(maxVisible, this.maxVisible) == maxVisible) { 79 this._direction = 'DOWN'; 80 } 81 else if (Math.min(minVisible, this.minVisible) == minVisible && 82 Math.min(maxVisible, this.maxVisible) == maxVisible) { 83 this._direction = 'UP'; 84 } 85 this.minVisible = minVisible; 86 this.maxVisible = maxVisible; 87 let imagesOnScreen = maxVisible - minVisible + 1; 88 if (this.firstScreen) { 89 this.meanImagesOnScreen = imagesOnScreen; 90 this.firstScreen = false; 91 } 92 else { 93 const weight = 0.95; 94 this.meanImagesOnScreen = this.meanImagesOnScreen * weight + (1 - weight) * imagesOnScreen; 95 imagesOnScreen = Math.ceil(this.meanImagesOnScreen); 96 } 97 const visibleRangeSizeChanged = this.visibleRange.length !== imagesOnScreen; 98 this._visibleRange = new IndexRange(minVisible, maxVisible + 1); 99 if (visibleRangeSizeChanged) { 100 this.notifyObservers(); 101 } 102 } 103 notifyObservers() { 104 this.callbacks.forEach((callback) => callback()); 105 } 106} 107class PrefetchCount { 108 constructor(itemsOnScreen) { 109 this.MAX_SCREENS = 4; 110 this.MIN_SCREENS = 0.6; 111 this.min = 0; 112 this.max = 0; 113 this._prefetchCountValue = 0; 114 this._currentLimit = 0; 115 this._maxRatio = 0.5; 116 this.itemsOnScreen = itemsOnScreen; 117 this.itemsOnScreen.register(() => { 118 this.updateLimits(); 119 }); 120 } 121 get prefetchCountValue() { 122 return this._prefetchCountValue; 123 } 124 set prefetchCountValue(v) { 125 this._prefetchCountValue = v; 126 Logger.log(`{"tm":${Date.now()},"prefetch_count":${v}}`); 127 } 128 get currentLimit() { 129 return this._currentLimit; 130 } 131 get maxRatio() { 132 return this._maxRatio; 133 } 134 set maxRatio(value) { 135 this._maxRatio = value; 136 this.updateCurrentLimit(); 137 } 138 getPrefetchCountByRatio(ratio) { 139 return this.min + Math.ceil(ratio * (this.currentLimit - this.min)); 140 } 141 getRangeToPrefetch(totalCount) { 142 const visibleRange = this.itemsOnScreen.visibleRange; 143 let start = 0; 144 let end = 0; 145 switch (this.itemsOnScreen.direction) { 146 case 'UNKNOWN': 147 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue)); 148 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue)); 149 break; 150 case 'UP': 151 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue)); 152 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue * 0.5)); 153 break; 154 case 'DOWN': 155 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue * 0.5)); 156 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue)); 157 break; 158 } 159 return new IndexRange(start, end); 160 } 161 updateLimits() { 162 this.min = Math.ceil(this.itemsOnScreen.meanValue * this.MIN_SCREENS); 163 this.max = Math.max(this.min, Math.ceil(this.MAX_SCREENS * this.itemsOnScreen.meanValue)); 164 this.updateCurrentLimit(); 165 } 166 updateCurrentLimit() { 167 this._currentLimit = Math.max(this.min, Math.ceil(this.max * this._maxRatio)); 168 } 169} 170class PrefetchRangeEvaluator { 171 constructor() { 172 this.itemsOnScreen = new ItemsOnScreenProvider(); 173 this.prefetchCount = new PrefetchCount(this.itemsOnScreen); 174 this.fetchedRegistry = new FetchedRegistry(); 175 this.prefetchRangeRatio = new PrefetchRangeRatio(this.itemsOnScreen, this.fetchedRegistry); 176 this.totalItems = 0; 177 this.rangeToPrefetch_ = new IndexRange(0, 0); 178 } 179 get rangeToPrefetch() { 180 return this.rangeToPrefetch_; 181 } 182 setTotal(totalItems) { 183 this.totalItems = totalItems; 184 } 185 visibleRangeChanged(minVisible, maxVisible) { 186 this.itemsOnScreen.update(minVisible, maxVisible); 187 Logger.log(`visibleAreaChanged itemsOnScreen=${Math.abs(maxVisible - minVisible) + 1}, meanImagesOnScreen=${this.itemsOnScreen.meanValue}, prefetchCountCurrentLimit=${this.prefetchCount.currentLimit}, prefetchCountMaxRatio=${this.prefetchCount.maxRatio}`); 188 this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('visible-area-changed'); 189 this.rangeToPrefetch_ = this.prefetchCount.getRangeToPrefetch(this.totalItems); 190 this.fetchedRegistry.updateRangeToPrefetch(this.rangeToPrefetch_); 191 } 192 itemPrefetched(index, fetchDuration) { 193 let maxRatioChanged = false; 194 if (this.prefetchRangeRatio.update(fetchDuration) === 'ratio-changed') { 195 this.prefetchCount.maxRatio = this.prefetchRangeRatio.maxRatio; 196 maxRatioChanged = true; 197 Logger.log(`choosePrefetchCountLimit prefetchCountMaxRatio=${this.prefetchCount.maxRatio}, prefetchCountCurrentLimit=${this.prefetchCount.currentLimit}`); 198 } 199 this.fetchedRegistry.addFetched(index); 200 this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('resolved', maxRatioChanged); 201 this.rangeToPrefetch_ = this.prefetchCount.getRangeToPrefetch(this.totalItems); 202 this.fetchedRegistry.updateRangeToPrefetch(this.rangeToPrefetch_); 203 } 204 evaluatePrefetchCount(event, maxRatioChanged) { 205 const ratio = this.prefetchRangeRatio.calculateRatio(this.prefetchCount.prefetchCountValue, this.totalItems); 206 let evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio); 207 let nextRatio; 208 if (maxRatioChanged) { 209 nextRatio = this.prefetchRangeRatio.calculateRatio(evaluatedPrefetchCount, this.totalItems); 210 evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(nextRatio); 211 } 212 if (this.prefetchRangeRatio.range.contains(ratio) || 213 (event === 'resolved' && evaluatedPrefetchCount <= this.prefetchCount.prefetchCountValue)) { 214 return this.prefetchCount.prefetchCountValue; 215 } 216 this.prefetchRangeRatio.updateRatioRange(ratio); 217 Logger.log(`evaluatePrefetchCount prefetchCount=${evaluatedPrefetchCount}, ratio=${ratio}, nextRatio=${nextRatio}, hysteresisRange=${this.prefetchRangeRatio.range}`); 218 return evaluatedPrefetchCount; 219 } 220} 221class PrefetchRangeRatio { 222 constructor(itemsOnScreen, fetchedRegistry) { 223 this.TOLERANCE_RANGES = [ 224 { 225 leftToleranceEdge: 180, 226 rightToleranceEdge: 250, 227 prefetchCountMaxRatioLeft: 0.5, 228 prefetchCountMaxRatioRight: 1, 229 }, 230 { 231 leftToleranceEdge: 3000, 232 rightToleranceEdge: 4000, 233 prefetchCountMaxRatioLeft: 1, 234 prefetchCountMaxRatioRight: 0.25, 235 }, 236 ]; 237 this.ACTIVE_DEGREE = 0.5; 238 this.VISIBLE_DEGREE = 2.5; 239 this.meanPrefetchTime = 0; 240 this.leftToleranceEdge = Number.MIN_VALUE; 241 this.rightToleranceEdge = 250; 242 this.oldRatio = 0; 243 this._range = RatioRange.newEmpty(); 244 this._maxRatio = 0.5; 245 this.itemsOnScreen = itemsOnScreen; 246 this.fetchedRegistry = fetchedRegistry; 247 } 248 get range() { 249 return this._range; 250 } 251 get maxRatio() { 252 return this._maxRatio; 253 } 254 updateTiming(prefetchDuration) { 255 if (prefetchDuration > 20) { 256 const weight = 0.95; 257 this.meanPrefetchTime = this.meanPrefetchTime * weight + (1 - weight) * prefetchDuration; 258 } 259 Logger.log(`prefetchDifference prefetchDur=${prefetchDuration}, meanPrefetchDur=${this.meanPrefetchTime}`); 260 } 261 update(prefetchDuration) { 262 this.updateTiming(prefetchDuration); 263 if (this.meanPrefetchTime >= this.leftToleranceEdge && this.meanPrefetchTime <= this.rightToleranceEdge) { 264 return 'ratio-not-changed'; 265 } 266 let ratioChanged = false; 267 if (this.meanPrefetchTime > this.rightToleranceEdge) { 268 for (let i = 0; i < this.TOLERANCE_RANGES.length; i++) { 269 const limit = this.TOLERANCE_RANGES[i]; 270 if (this.meanPrefetchTime > limit.rightToleranceEdge) { 271 ratioChanged = true; 272 this._maxRatio = limit.prefetchCountMaxRatioRight; 273 this.leftToleranceEdge = limit.leftToleranceEdge; 274 if (i + 1 != this.TOLERANCE_RANGES.length) { 275 this.rightToleranceEdge = this.TOLERANCE_RANGES[i + 1].rightToleranceEdge; 276 } 277 else { 278 this.rightToleranceEdge = Number.MAX_VALUE; 279 } 280 } 281 } 282 } 283 else if (this.meanPrefetchTime < this.leftToleranceEdge) { 284 for (let i = this.TOLERANCE_RANGES.length - 1; i >= 0; i--) { 285 const limit = this.TOLERANCE_RANGES[i]; 286 if (this.meanPrefetchTime < limit.leftToleranceEdge) { 287 ratioChanged = true; 288 this._maxRatio = limit.prefetchCountMaxRatioLeft; 289 this.rightToleranceEdge = limit.rightToleranceEdge; 290 if (i != 0) { 291 this.leftToleranceEdge = this.TOLERANCE_RANGES[i - 1].leftToleranceEdge; 292 } 293 else { 294 this.leftToleranceEdge = Number.MIN_VALUE; 295 } 296 } 297 } 298 } 299 return ratioChanged ? 'ratio-changed' : 'ratio-not-changed'; 300 } 301 calculateRatio(prefetchCount, totalCount) { 302 const visibleRange = this.itemsOnScreen.visibleRange; 303 const start = Math.max(0, visibleRange.start - prefetchCount); 304 const end = Math.min(totalCount, visibleRange.end + prefetchCount); 305 const evaluatedPrefetchRange = new IndexRange(start, end); 306 const completedActive = this.fetchedRegistry.getFetchedInRange(evaluatedPrefetchRange); 307 const completedVisible = this.fetchedRegistry.getFetchedInRange(visibleRange); 308 return Math.min(1, Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE) * 309 Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE)); 310 } 311 updateRatioRange(ratio) { 312 if (ratio > this.oldRatio) { 313 this._range = new RatioRange(new RangeEdge(this.oldRatio, false), new RangeEdge(ratio, true)); 314 } 315 else { 316 this._range = new RatioRange(new RangeEdge(ratio, true), new RangeEdge(this.oldRatio, false)); 317 } 318 this.oldRatio = ratio; 319 } 320} 321class DefaultTimeProvider { 322 getCurrent() { 323 return Date.now(); 324 } 325} 326class Prefetcher { 327 constructor(prefetchRangeEvaluator, timeProvider) { 328 this.datasource = null; 329 this.prefetchRangeEvaluator = prefetchRangeEvaluator; 330 this.timeProvider = timeProvider; 331 } 332 setDataSource(ds) { 333 this.datasource = ds; 334 this.prefetchRangeEvaluator.setTotal(ds.totalCount()); 335 } 336 visibleAreaChanged(minVisible, maxVisible) { 337 if (!this.datasource) { 338 throw new Error('No data source'); 339 } 340 const oldRangeToPrefetch = this.prefetchRangeEvaluator.rangeToPrefetch; 341 this.prefetchRangeEvaluator.visibleRangeChanged(minVisible, maxVisible); 342 this.prefetchDifference(oldRangeToPrefetch); 343 this.cancelDifference(oldRangeToPrefetch); 344 } 345 prefetchDifference(oldRange) { 346 this.prefetchRangeEvaluator.rangeToPrefetch.subtract(oldRange).forEachIndex((index) => { 347 const prefetchStart = this.timeProvider.getCurrent(); 348 const prefetchedCallback = () => { 349 if (!this.prefetchRangeEvaluator.rangeToPrefetch.contains(index)) { 350 return; 351 } 352 const oldRangeToPrefetch = this.prefetchRangeEvaluator.rangeToPrefetch; 353 const prefetchDuration = this.timeProvider.getCurrent() - prefetchStart; 354 this.prefetchRangeEvaluator.itemPrefetched(index, prefetchDuration); 355 this.prefetchDifference(oldRangeToPrefetch); 356 }; 357 const prefetchResponse = this.datasource.prefetch(index); 358 if (prefetchResponse instanceof Promise) { 359 prefetchResponse.then(prefetchedCallback); 360 } 361 else { 362 prefetchedCallback(); 363 } 364 }); 365 } 366 cancelDifference(oldRangeToPrefetch) { 367 if (!this.datasource.cancel || this.prefetchRangeEvaluator.rangeToPrefetch.length > oldRangeToPrefetch.length) { 368 return; 369 } 370 oldRangeToPrefetch.subtract(this.prefetchRangeEvaluator.rangeToPrefetch).forEachIndex((index) => { 371 this.datasource.cancel(index); 372 }); 373 } 374} 375class Logger { 376 static log(message) { } 377} 378class IndexRange { 379 constructor(start, end) { 380 this.start = start; 381 this.end = end; 382 if (this.start > this.end) { 383 throw new Error('Invalid range'); 384 } 385 } 386 get length() { 387 return this.end - this.start; 388 } 389 contains(value) { 390 if (typeof value === 'object') { 391 return this.start <= value.start && value.end <= this.end; 392 } 393 else { 394 return this.start <= value && value < this.end; 395 } 396 } 397 subtract(other) { 398 const result = new IndexRangeArray(); 399 if (other.start > this.start) { 400 result.push(new IndexRange(this.start, Math.min(this.end, other.start))); 401 } 402 if (other.end < this.end) { 403 result.push(new IndexRange(Math.max(other.end, this.start), this.end)); 404 } 405 return result; 406 } 407 expandedWith(other) { 408 return new IndexRange(Math.min(this.start, other.start), Math.max(this.end, other.end)); 409 } 410 forEachIndex(callback) { 411 for (let i = this.start; i < this.end; ++i) { 412 callback(i); 413 } 414 } 415 format() { 416 return `[${this.start}..${this.end})`; 417 } 418} 419class IndexRangeArray extends Array { 420 forEachIndex(callback) { 421 this.forEach((range) => { 422 range.forEachIndex(callback); 423 }); 424 } 425} 426class RangeEdge { 427 constructor(value, inclusive) { 428 this.value = value; 429 this.inclusive = inclusive; 430 } 431} 432class RatioRange { 433 constructor(start, end) { 434 this.start = start; 435 this.end = end; 436 if (this.start.value > this.end.value) { 437 throw new Error(`RatioRange: ${this.start.value} > ${this.end.value}`); 438 } 439 } 440 static newEmpty() { 441 return new RatioRange(new RangeEdge(0, false), new RangeEdge(0, false)); 442 } 443 contains(point) { 444 if (point == this.start.value) { 445 return this.start.inclusive; 446 } 447 if (point == this.end.value) { 448 return this.end.inclusive; 449 } 450 return this.start.value < point && point < this.end.value; 451 } 452 toString() { 453 return `${this.start.inclusive ? '[' : '('}${this.start.value}, ${this.end.value}${this.end.inclusive ? ']' : ')'}`; 454 } 455} 456 457export default { BasicPrefetcher }; 458