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 const itemsOnScreen = new ItemsOnScreenProvider(); 19 const fetchedRegistry = new FetchedRegistry(); 20 const fetchingRegistry = new FetchingRegistry(); 21 const prefetchRangeRatio = new PrefetchRangeRatio(itemsOnScreen, fetchedRegistry, fetchingRegistry); 22 const prefetchCount = new PrefetchCount(itemsOnScreen, prefetchRangeRatio); 23 const evaluator = new FetchingRangeEvaluator(itemsOnScreen, prefetchCount, prefetchRangeRatio, fetchedRegistry); 24 this.fetchingDriver = new FetchingDriver(fetchedRegistry, fetchingRegistry, evaluator, new DefaultTimeProvider()); 25 this.fetchingDriver.setDataSource(ds); 26 } 27 setDataSource(ds) { 28 this.fetchingDriver.setDataSource(ds); 29 } 30 visibleAreaChanged(minVisible, maxVisible) { 31 this.fetchingDriver.visibleAreaChanged(minVisible, maxVisible); 32 } 33} 34class DataSourceObserver { 35 constructor(simpleChangeListener) { 36 this.simpleChangeListener = simpleChangeListener; 37 } 38 onDataReloaded() { 39 this.simpleChangeListener.batchUpdate([ 40 { 41 kind: 'reloaded', 42 totalCount: this.dataSource.totalCount(), 43 }, 44 ]); 45 } 46 onDataAdded(index) { 47 this.simpleChangeListener.batchUpdate([ 48 { 49 kind: 'added', 50 startIndex: index, 51 count: 1, 52 }, 53 ]); 54 } 55 onDataAdd(index) { 56 this.onDataAdded(index); 57 } 58 onDataMoved(from, to) { 59 this.simpleChangeListener.batchUpdate([ 60 { 61 kind: 'swapped', 62 a: from, 63 b: to, 64 }, 65 ]); 66 } 67 onDataMove(from, to) { 68 this.onDataMoved(from, to); 69 } 70 onDataDeleted(index) { 71 this.simpleChangeListener.batchUpdate([ 72 { 73 kind: 'deleted', 74 startIndex: index, 75 count: 1, 76 }, 77 ]); 78 } 79 onDataDelete(index) { 80 this.onDataDeleted(index); 81 } 82 onDataChanged(index) { 83 this.simpleChangeListener.batchUpdate([ 84 { 85 kind: 'updated', 86 index, 87 }, 88 ]); 89 } 90 onDataChange(index) { 91 this.onDataChanged(index); 92 } 93 onDatasetChange(dataOperations) { 94 const operations = []; 95 dataOperations.forEach((operation) => { 96 switch (operation.type) { 97 case 'add': 98 case 'delete': 99 if (operation.count === undefined || operation.count > 0) { 100 operations.push({ 101 kind: operation.type === 'add' ? 'added' : 'deleted', 102 startIndex: operation.index, 103 count: operation.count ?? 1, 104 }); 105 } 106 break; 107 case 'change': 108 operations.push({ 109 kind: 'updated', 110 index: operation.index, 111 }); 112 break; 113 case 'reload': 114 operations.push({ 115 kind: 'reloaded', 116 totalCount: this.dataSource.totalCount(), 117 }); 118 break; 119 case 'exchange': 120 operations.push({ 121 kind: 'swapped', 122 a: operation.index.start, 123 b: operation.index.end, 124 }); 125 break; 126 case 'move': 127 operations.push({ 128 kind: 'moved', 129 from: operation.index.from, 130 to: operation.index.to, 131 }); 132 break; 133 default: 134 assertNever(operation); 135 } 136 }); 137 this.simpleChangeListener.batchUpdate(operations); 138 } 139 setDataSource(dataSource) { 140 if (this.dataSource) { 141 this.dataSource.unregisterDataChangeListener(this); 142 } 143 this.dataSource = dataSource; 144 this.dataSource.registerDataChangeListener(this); 145 this.onDataReloaded(); 146 } 147} 148class FetchingRegistry { 149 constructor() { 150 this.fetches = new Map(); 151 this.fetching = new Map(); 152 this.fetchesBefore = new Map(); 153 this.fetchCounter = 0; 154 } 155 registerFetch(index) { 156 let fetchId = this.fetching.get(index); 157 if (fetchId !== undefined) { 158 return fetchId; 159 } 160 fetchId = ++this.fetchCounter; 161 this.fetching.set(index, fetchId); 162 this.fetches.set(fetchId, index); 163 this.fetchesBefore.set(index, this.fetches.size); 164 return fetchId; 165 } 166 getItem(fetchId) { 167 return this.fetches.get(fetchId); 168 } 169 deleteFetch(fetchId) { 170 const index = this.fetches.get(fetchId); 171 if (index !== undefined) { 172 this.fetching.delete(index); 173 this.fetches.delete(fetchId); 174 } 175 } 176 deleteFetchByItem(index) { 177 const fetchId = this.fetching.get(index); 178 if (fetchId !== undefined) { 179 this.fetching.delete(index); 180 this.fetches.delete(fetchId); 181 } 182 } 183 isFetchingItem(index) { 184 return this.fetching.has(index); 185 } 186 incrementAllIndexesGreaterThen(value) { 187 this.offsetAllIndexesGreaterThen(value, 1); 188 } 189 getAllIndexes() { 190 const set = new Set(); 191 this.fetching.forEach((fetchId, itemIndex) => set.add(itemIndex)); 192 return set; 193 } 194 getFetchesCount() { 195 return this.fetches.size; 196 } 197 isFetchLatecomer(index, threshold) { 198 return this.fetchesBefore.get(index) > threshold; 199 } 200 offsetAllIndexesGreaterThen(value, offset) { 201 const newFetching = new Map(); 202 this.fetches.forEach((index, fetchId) => { 203 const toSet = index > value ? index + offset : index; 204 newFetching.set(toSet, fetchId); 205 this.fetches.set(fetchId, toSet); 206 }); 207 this.fetching = newFetching; 208 } 209 decrementAllIndexesGreaterThen(value) { 210 this.offsetAllIndexesGreaterThen(value, -1); 211 } 212} 213class FetchedRegistry { 214 constructor() { 215 this.fetchedIndexes = new Set(); 216 this.rangeToFetchInternal = new IndexRange(0, 0); 217 this.missedIndexes = new Set(); 218 } 219 get rangeToFetch() { 220 return this.rangeToFetchInternal; 221 } 222 addFetched(index) { 223 if (this.rangeToFetch.contains(index)) { 224 this.fetchedIndexes.add(index); 225 this.missedIndexes.delete(index); 226 } 227 } 228 removeFetched(index) { 229 if (this.rangeToFetch.contains(index)) { 230 this.fetchedIndexes.delete(index); 231 this.missedIndexes.add(index); 232 } 233 } 234 has(index) { 235 return this.fetchedIndexes.has(index); 236 } 237 getFetchedInRange(range) { 238 let fetched = 0; 239 range.forEachIndex((index) => { 240 fetched += this.fetchedIndexes.has(index) ? 1 : 0; 241 }); 242 return fetched; 243 } 244 updateRangeToFetch(fetchRange) { 245 this.rangeToFetch.subtract(fetchRange).forEachIndex((index) => { 246 this.fetchedIndexes.delete(index); 247 }); 248 this.rangeToFetchInternal = fetchRange; 249 this.missedIndexes.clear(); 250 this.rangeToFetch.forEachIndex((index) => { 251 if (!this.fetchedIndexes.has(index)) { 252 this.missedIndexes.add(index); 253 } 254 }); 255 } 256 getItemsToFetch() { 257 return new Set(this.missedIndexes); 258 } 259 incrementFetchedGreaterThen(value, newFetchRange) { 260 this.offsetAllGreaterThen(value, 1); 261 this.updateRangeToFetch(newFetchRange); 262 } 263 decrementFetchedGreaterThen(value, newFetchRange) { 264 this.offsetAllGreaterThen(value, -1); 265 this.updateRangeToFetch(newFetchRange); 266 } 267 offsetAllGreaterThen(value, offset) { 268 const updated = new Set(); 269 this.fetchedIndexes.forEach((index) => { 270 updated.add(index > value ? index + offset : index); 271 }); 272 this.fetchedIndexes = updated; 273 } 274 clearFetched(newFetchRange) { 275 this.fetchedIndexes.clear(); 276 this.updateRangeToFetch(newFetchRange); 277 } 278} 279class ItemsOnScreenProvider { 280 constructor() { 281 this.firstScreen = true; 282 this.meanImagesOnScreen = 0; 283 this.minVisible = 0; 284 this.maxVisible = 0; 285 this.directionInternal = 'UNKNOWN'; 286 this.speedInternal = 0; 287 this.lastUpdateTimestamp = 0; 288 this.visibleRangeInternal = new IndexRange(0, 0); 289 this.callbacks = []; 290 } 291 register(callback) { 292 this.callbacks.push(callback); 293 } 294 get visibleRange() { 295 return this.visibleRangeInternal; 296 } 297 get meanValue() { 298 return this.meanImagesOnScreen; 299 } 300 get direction() { 301 return this.directionInternal; 302 } 303 get speed() { 304 return this.speedInternal; 305 } 306 updateSpeed(minVisible, maxVisible) { 307 const timeDifference = Date.now() - this.lastUpdateTimestamp; 308 if (timeDifference > 0) { 309 const speedTau = 100; 310 const speedWeight = 1 - Math.exp(-timeDifference / speedTau); 311 const distance = minVisible + (maxVisible - minVisible) / 2 - (this.minVisible + (this.maxVisible - this.minVisible) / 2); 312 const rawSpeed = Math.abs(distance / timeDifference) * 1000; 313 this.speedInternal = speedWeight * rawSpeed + (1 - speedWeight) * this.speedInternal; 314 } 315 } 316 update(minVisible, maxVisible) { 317 if (minVisible !== this.minVisible || maxVisible !== this.maxVisible) { 318 if (Math.max(minVisible, this.minVisible) === minVisible && 319 Math.max(maxVisible, this.maxVisible) === maxVisible) { 320 this.directionInternal = 'DOWN'; 321 } 322 else if (Math.min(minVisible, this.minVisible) === minVisible && 323 Math.min(maxVisible, this.maxVisible) === maxVisible) { 324 this.directionInternal = 'UP'; 325 } 326 } 327 let imagesOnScreen = maxVisible - minVisible + 1; 328 let oldMeanImagesOnScreen = this.meanImagesOnScreen; 329 if (this.firstScreen) { 330 this.meanImagesOnScreen = imagesOnScreen; 331 this.firstScreen = false; 332 this.lastUpdateTimestamp = Date.now(); 333 } 334 else { 335 { 336 const imagesWeight = 0.95; 337 this.meanImagesOnScreen = this.meanImagesOnScreen * imagesWeight + (1 - imagesWeight) * imagesOnScreen; 338 } 339 this.updateSpeed(minVisible, maxVisible); 340 } 341 this.minVisible = minVisible; 342 this.maxVisible = maxVisible; 343 const visibleRangeSizeChanged = Math.ceil(oldMeanImagesOnScreen) !== Math.ceil(this.meanImagesOnScreen); 344 this.visibleRangeInternal = new IndexRange(minVisible, maxVisible + 1); 345 if (visibleRangeSizeChanged) { 346 this.notifyObservers(); 347 } 348 this.lastUpdateTimestamp = Date.now(); 349 } 350 notifyObservers() { 351 this.callbacks.forEach((callback) => callback()); 352 } 353} 354class PrefetchCount { 355 constructor(itemsOnScreen, prefetchRangeRatio, logger = dummyLogger) { 356 this.itemsOnScreen = itemsOnScreen; 357 this.prefetchRangeRatio = prefetchRangeRatio; 358 this.logger = logger; 359 this.MAX_SCREENS = 4; 360 this.speedCoef = 2.5; 361 this.maxItems = 0; 362 this.prefetchCountValueInternal = 0; 363 this.currentMaxItemsInternal = 0; 364 this.currentMinItemsInternal = 0; 365 this.itemsOnScreen = itemsOnScreen; 366 this.itemsOnScreen.register(() => { 367 this.updateLimits(); 368 }); 369 this.prefetchRangeRatio.register(() => { 370 this.updateLimits(); 371 }); 372 } 373 get prefetchCountValue() { 374 return this.prefetchCountValueInternal; 375 } 376 set prefetchCountValue(v) { 377 this.prefetchCountValueInternal = v; 378 this.logger.debug(`{"tm":${Date.now()},"prefetch_count":${v}}`); 379 } 380 get currentMaxItems() { 381 return this.currentMaxItemsInternal; 382 } 383 get currentMinItems() { 384 return this.currentMinItemsInternal; 385 } 386 getPrefetchCountByRatio(ratio) { 387 this.itemsOnScreen.updateSpeed(this.itemsOnScreen.visibleRange.start, this.itemsOnScreen.visibleRange.end - 1); 388 const minItems = Math.min(this.currentMaxItems, Math.ceil(this.speedCoef * this.itemsOnScreen.speed * this.currentMaxItems)); 389 const prefetchCount = minItems + Math.ceil(ratio * (this.currentMaxItems - minItems)); 390 this.logger.debug(`speed: ${this.itemsOnScreen.speed}, minItems: ${minItems}, ratio: ${ratio}, prefetchCount: ${prefetchCount}`); 391 return prefetchCount; 392 } 393 getRangeToFetch(totalCount) { 394 const visibleRange = this.itemsOnScreen.visibleRange; 395 let start = 0; 396 let end = 0; 397 switch (this.itemsOnScreen.direction) { 398 case 'UNKNOWN': 399 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue)); 400 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue)); 401 break; 402 case 'UP': 403 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue)); 404 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue * 0.5)); 405 break; 406 case 'DOWN': 407 start = Math.max(0, visibleRange.start - Math.round(this.prefetchCountValue * 0.5)); 408 end = Math.min(totalCount, visibleRange.end + Math.round(this.prefetchCountValue)); 409 break; 410 } 411 if (start > end) { 412 start = end; 413 } 414 return new IndexRange(start, end); 415 } 416 updateLimits() { 417 this.maxItems = Math.max(this.currentMinItems, Math.ceil(this.MAX_SCREENS * this.itemsOnScreen.meanValue)); 418 this.updateCurrentLimit(); 419 } 420 updateCurrentLimit() { 421 this.currentMaxItemsInternal = Math.max(this.currentMinItems, Math.ceil(this.maxItems * this.prefetchRangeRatio.maxRatio)); 422 this.currentMinItemsInternal = Math.ceil(this.maxItems * this.prefetchRangeRatio.minRatio); 423 } 424} 425class FetchingRangeEvaluator { 426 constructor(itemsOnScreen, prefetchCount, prefetchRangeRatio, fetchedRegistry, logger = dummyLogger) { 427 this.itemsOnScreen = itemsOnScreen; 428 this.prefetchCount = prefetchCount; 429 this.prefetchRangeRatio = prefetchRangeRatio; 430 this.fetchedRegistry = fetchedRegistry; 431 this.logger = logger; 432 this.totalItems = 0; 433 } 434 updateRangeToFetch(whatHappened) { 435 switch (whatHappened.kind) { 436 case 'visible-area-changed': 437 this.onVisibleAreaChange(whatHappened.minVisible, whatHappened.maxVisible); 438 break; 439 case 'item-fetched': 440 this.onItemFetched(whatHappened.itemIndex, whatHappened.fetchDuration); 441 break; 442 case 'collection-changed': 443 this.onCollectionChanged(whatHappened.totalCount); 444 break; 445 case 'item-added': 446 this.onItemAdded(whatHappened.itemIndex); 447 break; 448 case 'item-removed': 449 this.onItemDeleted(whatHappened.itemIndex); 450 break; 451 default: 452 assertNever(whatHappened); 453 } 454 } 455 onVisibleAreaChange(minVisible, maxVisible) { 456 const oldVisibleRange = this.itemsOnScreen.visibleRange; 457 this.itemsOnScreen.update(minVisible, maxVisible); 458 this.logger.debug(`visibleAreaChanged itemsOnScreen=${this.itemsOnScreen.visibleRange.length}, meanImagesOnScreen=${this.itemsOnScreen.meanValue}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}, prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}`); 459 if (!oldVisibleRange.equals(this.itemsOnScreen.visibleRange)) { 460 this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('visible-area-changed'); 461 const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems); 462 this.fetchedRegistry.updateRangeToFetch(rangeToFetch); 463 } 464 } 465 onItemFetched(index, fetchDuration) { 466 if (!this.fetchedRegistry.rangeToFetch.contains(index)) { 467 return; 468 } 469 this.logger.debug(`onItemFetched`); 470 let maxRatioChanged = false; 471 if (this.prefetchRangeRatio.update(index, fetchDuration) === 'ratio-changed') { 472 maxRatioChanged = true; 473 this.logger.debug(`choosePrefetchCountLimit prefetchCountMaxRatio=${this.prefetchRangeRatio.maxRatio}, prefetchCountMinRatio=${this.prefetchRangeRatio.minRatio}, prefetchCountCurrentLimit=${this.prefetchCount.currentMaxItems}`); 474 } 475 this.fetchedRegistry.addFetched(index); 476 this.prefetchCount.prefetchCountValue = this.evaluatePrefetchCount('resolved', maxRatioChanged); 477 const rangeToFetch = this.prefetchCount.getRangeToFetch(this.totalItems); 478 this.fetchedRegistry.updateRangeToFetch(rangeToFetch); 479 } 480 evaluatePrefetchCount(event, maxRatioChanged) { 481 let ratio = this.prefetchRangeRatio.calculateRatio(this.prefetchCount.prefetchCountValue, this.totalItems); 482 let evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio); 483 if (maxRatioChanged) { 484 ratio = this.prefetchRangeRatio.calculateRatio(evaluatedPrefetchCount, this.totalItems); 485 evaluatedPrefetchCount = this.prefetchCount.getPrefetchCountByRatio(ratio); 486 } 487 if (!this.prefetchRangeRatio.hysteresisEnabled) { 488 if (event === 'resolved') { 489 this.prefetchRangeRatio.updateRatioRange(ratio); 490 this.prefetchRangeRatio.hysteresisEnabled = true; 491 } 492 else if (event === 'visible-area-changed') { 493 this.prefetchRangeRatio.oldRatio = ratio; 494 } 495 } 496 else if (this.prefetchRangeRatio.range.contains(ratio)) { 497 return this.prefetchCount.prefetchCountValue; 498 } 499 else { 500 if (event === 'resolved') { 501 this.prefetchRangeRatio.updateRatioRange(ratio); 502 } 503 else if (event === 'visible-area-changed') { 504 this.prefetchRangeRatio.setEmptyRange(); 505 this.prefetchRangeRatio.oldRatio = ratio; 506 this.prefetchRangeRatio.hysteresisEnabled = false; 507 } 508 } 509 this.logger.debug(`evaluatePrefetchCount event=${event}, ${this.prefetchRangeRatio.hysteresisEnabled ? 'inHysteresis' : 'setHysteresis'} prefetchCount=${evaluatedPrefetchCount}, ratio=${ratio}, hysteresisRange=${this.prefetchRangeRatio.range}`); 510 return evaluatedPrefetchCount; 511 } 512 onCollectionChanged(totalCount) { 513 this.totalItems = Math.max(0, totalCount); 514 let newRangeToFetch; 515 if (this.fetchedRegistry.rangeToFetch.length > 0) { 516 newRangeToFetch = this.itemsOnScreen.visibleRange; 517 } 518 else { 519 newRangeToFetch = this.fetchedRegistry.rangeToFetch; 520 } 521 if (newRangeToFetch.end > this.totalItems) { 522 const end = this.totalItems; 523 const start = newRangeToFetch.start < end ? newRangeToFetch.start : end; 524 newRangeToFetch = new IndexRange(start, end); 525 } 526 this.fetchedRegistry.clearFetched(newRangeToFetch); 527 } 528 onItemDeleted(itemIndex) { 529 if (this.totalItems === 0) { 530 return; 531 } 532 this.totalItems--; 533 this.fetchedRegistry.removeFetched(itemIndex); 534 const end = this.fetchedRegistry.rangeToFetch.end < this.totalItems ? this.fetchedRegistry.rangeToFetch.end : this.totalItems; 535 const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end); 536 this.fetchedRegistry.decrementFetchedGreaterThen(itemIndex, rangeToFetch); 537 } 538 onItemAdded(itemIndex) { 539 this.totalItems++; 540 if (itemIndex > this.fetchedRegistry.rangeToFetch.end) { 541 return; 542 } 543 const end = this.fetchedRegistry.rangeToFetch.end + 1; 544 const rangeToFetch = new IndexRange(this.fetchedRegistry.rangeToFetch.start, end); 545 this.fetchedRegistry.incrementFetchedGreaterThen(itemIndex - 1, rangeToFetch); 546 } 547} 548class PrefetchRangeRatio { 549 constructor(itemsOnScreen, fetchedRegistry, fetchingRegistry, logger = dummyLogger) { 550 this.itemsOnScreen = itemsOnScreen; 551 this.fetchedRegistry = fetchedRegistry; 552 this.fetchingRegistry = fetchingRegistry; 553 this.logger = logger; 554 this.TOLERANCE_RANGES = [ 555 { 556 leftToleranceEdge: 140, 557 rightToleranceEdge: 290, 558 prefetchCountMinRatioLeft: 0.5, 559 prefetchCountMaxRatioLeft: 0.5, 560 prefetchCountMinRatioRight: 0.25, 561 prefetchCountMaxRatioRight: 1, 562 }, 563 { 564 leftToleranceEdge: 3000, 565 rightToleranceEdge: 4000, 566 prefetchCountMinRatioLeft: 0.25, 567 prefetchCountMaxRatioLeft: 1, 568 prefetchCountMinRatioRight: 0.25, 569 prefetchCountMaxRatioRight: 0.25, 570 }, 571 ]; 572 this.ACTIVE_DEGREE = 0; 573 this.VISIBLE_DEGREE = 2.5; 574 this.meanPrefetchTime = 0; 575 this.leftToleranceEdge = Number.MIN_VALUE; 576 this.rightToleranceEdge = 250; 577 this.callbacks = []; 578 this.rangeInternal = RatioRange.newEmpty(); 579 this.minRatioInternal = 0.25 * 0.6; 580 this.maxRatioInternal = 0.5; 581 this.hysteresisEnabledInternal = false; 582 this.oldRatioInternal = 0; 583 } 584 register(callback) { 585 this.callbacks.push(callback); 586 } 587 get range() { 588 return this.rangeInternal; 589 } 590 setEmptyRange() { 591 this.rangeInternal = RatioRange.newEmpty(); 592 } 593 get maxRatio() { 594 return this.maxRatioInternal; 595 } 596 get minRatio() { 597 return this.minRatioInternal; 598 } 599 get hysteresisEnabled() { 600 return this.hysteresisEnabledInternal; 601 } 602 set hysteresisEnabled(value) { 603 this.hysteresisEnabledInternal = value; 604 } 605 set oldRatio(ratio) { 606 this.oldRatioInternal = ratio; 607 } 608 get oldRatio() { 609 return this.oldRatioInternal; 610 } 611 updateTiming(index, prefetchDuration) { 612 const weight = 0.95; 613 const localPrefetchDuration = 20; 614 let isFetchLocal = prefetchDuration < localPrefetchDuration; 615 let isFetchLatecomer = this.fetchingRegistry.isFetchLatecomer(index, this.itemsOnScreen.meanValue); 616 if (!isFetchLocal && !isFetchLatecomer) { 617 this.meanPrefetchTime = this.meanPrefetchTime * weight + (1 - weight) * prefetchDuration; 618 } 619 this.logger.debug(`prefetchDifference prefetchDur=${prefetchDuration}, meanPrefetchDur=${this.meanPrefetchTime}, ` + 620 `isFetchLocal=${isFetchLocal}, isFetchLatecomer=${isFetchLatecomer}`); 621 } 622 update(index, prefetchDuration) { 623 this.updateTiming(index, prefetchDuration); 624 if (this.meanPrefetchTime >= this.leftToleranceEdge && this.meanPrefetchTime <= this.rightToleranceEdge) { 625 return 'ratio-not-changed'; 626 } 627 let ratioChanged = false; 628 if (this.meanPrefetchTime > this.rightToleranceEdge) { 629 ratioChanged = this.updateOnGreaterThanRight(); 630 } 631 else if (this.meanPrefetchTime < this.leftToleranceEdge) { 632 ratioChanged = this.updateOnLessThanLeft(); 633 } 634 if (ratioChanged) { 635 this.notifyObservers(); 636 } 637 return ratioChanged ? 'ratio-changed' : 'ratio-not-changed'; 638 } 639 updateOnLessThanLeft() { 640 let ratioChanged = false; 641 for (let i = this.TOLERANCE_RANGES.length - 1; i >= 0; i--) { 642 const limit = this.TOLERANCE_RANGES[i]; 643 if (this.meanPrefetchTime < limit.leftToleranceEdge) { 644 ratioChanged = true; 645 this.maxRatioInternal = limit.prefetchCountMaxRatioLeft; 646 this.minRatioInternal = limit.prefetchCountMinRatioLeft; 647 this.rightToleranceEdge = limit.rightToleranceEdge; 648 if (i !== 0) { 649 this.leftToleranceEdge = this.TOLERANCE_RANGES[i - 1].leftToleranceEdge; 650 } 651 else { 652 this.leftToleranceEdge = Number.MIN_VALUE; 653 } 654 } 655 } 656 return ratioChanged; 657 } 658 updateOnGreaterThanRight() { 659 let ratioChanged = false; 660 for (let i = 0; i < this.TOLERANCE_RANGES.length; i++) { 661 const limit = this.TOLERANCE_RANGES[i]; 662 if (this.meanPrefetchTime > limit.rightToleranceEdge) { 663 ratioChanged = true; 664 this.maxRatioInternal = limit.prefetchCountMaxRatioRight; 665 this.minRatioInternal = limit.prefetchCountMinRatioRight; 666 this.leftToleranceEdge = limit.leftToleranceEdge; 667 if (i + 1 !== this.TOLERANCE_RANGES.length) { 668 this.rightToleranceEdge = this.TOLERANCE_RANGES[i + 1].rightToleranceEdge; 669 } 670 else { 671 this.rightToleranceEdge = Number.MAX_VALUE; 672 } 673 } 674 } 675 return ratioChanged; 676 } 677 calculateRatio(prefetchCount, totalCount) { 678 const visibleRange = this.itemsOnScreen.visibleRange; 679 let start = 0; 680 let end = 0; 681 switch (this.itemsOnScreen.direction) { 682 case 'UNKNOWN': 683 start = Math.max(0, visibleRange.start - prefetchCount); 684 end = Math.min(totalCount, visibleRange.end + prefetchCount); 685 break; 686 case 'UP': 687 start = Math.max(0, visibleRange.start - prefetchCount); 688 end = Math.min(totalCount, visibleRange.end + Math.round(0.5 * prefetchCount)); 689 break; 690 case 'DOWN': 691 start = Math.max(0, visibleRange.start - Math.round(0.5 * prefetchCount)); 692 end = Math.min(totalCount, visibleRange.end + prefetchCount); 693 break; 694 } 695 const evaluatedPrefetchRange = new IndexRange(start, end); 696 const completedActive = this.fetchedRegistry.getFetchedInRange(evaluatedPrefetchRange); 697 const completedVisible = this.fetchedRegistry.getFetchedInRange(visibleRange); 698 if (evaluatedPrefetchRange.length === 0 || visibleRange.length === 0) { 699 return 0; 700 } 701 this.logger.debug(`active_degree=${this.ACTIVE_DEGREE}, visible_degree=${this.VISIBLE_DEGREE}`); 702 this.logger.debug(`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}`); 703 const ratio = Math.pow(completedActive / evaluatedPrefetchRange.length, this.ACTIVE_DEGREE) * 704 Math.pow(completedVisible / visibleRange.length, this.VISIBLE_DEGREE); 705 this.logger.debug(`calculateRatio ratio=${ratio}, completedActive=${completedActive}, evaluatedPrefetchRange.length=${evaluatedPrefetchRange.length}, ` + 706 `completedVisible=${completedVisible}, visibleRange.length=${visibleRange.length}`); 707 return Math.min(1, ratio); 708 } 709 updateRatioRange(ratio) { 710 if (ratio > this.oldRatioInternal) { 711 this.rangeInternal = new RatioRange(new RangeEdge(this.oldRatioInternal, false), new RangeEdge(ratio, true)); 712 } 713 else { 714 this.rangeInternal = new RatioRange(new RangeEdge(ratio, true), new RangeEdge(this.oldRatioInternal, false)); 715 } 716 this.oldRatioInternal = ratio; 717 } 718 notifyObservers() { 719 this.callbacks.forEach((callback) => callback()); 720 } 721} 722class DefaultTimeProvider { 723 getCurrent() { 724 return Date.now(); 725 } 726} 727const dummyDataSource = { 728 prefetch: () => { }, 729 totalCount: () => { 730 return 0; 731 }, 732 getData: () => { 733 return undefined; 734 }, 735 registerDataChangeListener: () => { }, 736 unregisterDataChangeListener: () => { }, 737}; 738const DELAY_TO_REPEAT_FETCH_AFTER_ERROR = 500; 739class FetchingDriver { 740 constructor(fetchedRegistry, fetches, prefetchRangeEvaluator, timeProvider, logger = dummyLogger, autostart = true) { 741 this.fetchedRegistry = fetchedRegistry; 742 this.fetches = fetches; 743 this.prefetchRangeEvaluator = prefetchRangeEvaluator; 744 this.timeProvider = timeProvider; 745 this.logger = logger; 746 this.dataSource = dummyDataSource; 747 this.dataSourceObserver = new DataSourceObserver(this); 748 this.singleFetch = (itemIndex) => { 749 if (this.fetches.isFetchingItem(itemIndex) || this.fetchedRegistry.has(itemIndex)) { 750 return; 751 } 752 const prefetchStart = this.timeProvider.getCurrent(); 753 const fetchId = this.fetches.registerFetch(itemIndex); 754 this.logger.info('to prefetch ' + itemIndex); 755 try { 756 const prefetchResponse = this.dataSource.prefetch(itemIndex); 757 if (!(prefetchResponse instanceof Promise)) { 758 this.fetchedCallback(fetchId, prefetchStart); 759 return; 760 } 761 prefetchResponse 762 .then(() => this.fetchedCallback(fetchId, prefetchStart)) 763 .catch((e) => { 764 this.errorOnFetchCallback(fetchId, e); 765 }); 766 } 767 catch (e) { 768 this.errorOnFetchCallback(fetchId, e); 769 } 770 }; 771 this.isPaused = !autostart; 772 this.prefetchRangeEvaluator = prefetchRangeEvaluator; 773 this.timeProvider = timeProvider; 774 } 775 get afterErrorDelay() { 776 return DELAY_TO_REPEAT_FETCH_AFTER_ERROR; 777 } 778 batchUpdate(operations) { 779 this.logger.info('batchUpdate called with ' + JSON.stringify(operations)); 780 try { 781 this.batchUpdateInternal(operations); 782 } 783 catch (e) { 784 reportError(this.logger, 'batchUpdate', e); 785 throw e; 786 } 787 } 788 batchUpdateInternal(operations) { 789 operations.forEach((operation) => { 790 switch (operation.kind) { 791 case 'deleted': 792 this.itemsDeleted(operation.startIndex, operation.count); 793 break; 794 case 'added': 795 this.itemsAdded(operation.startIndex, operation.count); 796 break; 797 case 'updated': 798 this.itemUpdated(operation.index); 799 break; 800 case 'reloaded': 801 this.collectionChanged(operation.totalCount); 802 break; 803 case 'swapped': 804 this.itemsSwapped(operation.a, operation.b); 805 break; 806 case 'moved': 807 this.itemMoved(operation.from, operation.to); 808 break; 809 } 810 }); 811 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 812 } 813 collectionChanged(totalCount) { 814 this.prefetchRangeEvaluator.updateRangeToFetch({ 815 kind: 'collection-changed', 816 totalCount: totalCount, 817 }); 818 } 819 itemUpdated(index) { 820 this.fetchedRegistry.removeFetched(index); 821 this.fetches.deleteFetchByItem(index); 822 } 823 itemsDeleted(index, count) { 824 for (let i = 0; i < count; i++) { 825 this.fetches.decrementAllIndexesGreaterThen(index); 826 this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-removed', itemIndex: index }); 827 } 828 } 829 itemsAdded(index, count) { 830 for (let i = 0; i < count; i++) { 831 this.fetches.incrementAllIndexesGreaterThen(index - 1); 832 this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'item-added', itemIndex: index }); 833 } 834 } 835 itemsSwapped(a, b) { 836 if (!this.fetchedRegistry.has(a) || !this.fetchedRegistry.has(b)) { 837 this.fetchedRegistry.removeFetched(a); 838 this.fetchedRegistry.removeFetched(b); 839 } 840 } 841 itemMoved(from, to) { 842 if (!this.fetchedRegistry.has(from) || !this.fetchedRegistry.has(to)) { 843 const rangeToFetch = this.fetchedRegistry.rangeToFetch; 844 this.itemsDeleted(from, 1); 845 this.itemsAdded(to, 1); 846 this.fetchedRegistry.updateRangeToFetch(rangeToFetch); 847 } 848 } 849 setDataSource(ds = dummyDataSource) { 850 this.logger.info(`setDataSource called with ${ds !== dummyDataSource ? 'a data source' : 'null or undefined'}`); 851 try { 852 this.setDataSourceInternal(ds); 853 } 854 catch (e) { 855 reportError(this.logger, 'setDataSource', e); 856 throw e; 857 } 858 } 859 setDataSourceInternal(ds) { 860 this.dataSource = ds ?? dummyDataSource; 861 this.dataSourceObserver.setDataSource(this.dataSource); 862 } 863 stop() { 864 this.logger.info('Stop called'); 865 try { 866 this.stopInternal(); 867 } 868 catch (e) { 869 reportError(this.logger, 'stop', e); 870 throw e; 871 } 872 } 873 stopInternal() { 874 if (this.isPaused) { 875 return; 876 } 877 this.isPaused = true; 878 this.cancel(this.fetches.getAllIndexes()); 879 } 880 start() { 881 this.logger.info('Start called'); 882 try { 883 this.startInternal(); 884 } 885 catch (e) { 886 reportError(this.logger, 'start', e); 887 throw e; 888 } 889 } 890 startInternal() { 891 if (!this.isPaused) { 892 return; 893 } 894 this.isPaused = false; 895 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 896 } 897 visibleAreaChanged(minVisible, maxVisible) { 898 this.logger.info(`visibleAreaChanged min: ${minVisible} max: ${maxVisible}`); 899 try { 900 this.visibleAreaChangedInternal(minVisible, maxVisible); 901 } 902 catch (e) { 903 reportError(this.logger, 'visibleAreaChanged', e); 904 throw e; 905 } 906 } 907 visibleAreaChangedInternal(minVisible, maxVisible) { 908 if (this.dataSource === dummyDataSource) { 909 throw new Error('No data source'); 910 } 911 const oldRangeToPrefetch = this.fetchedRegistry.rangeToFetch; 912 this.prefetchRangeEvaluator.updateRangeToFetch({ kind: 'visible-area-changed', minVisible, maxVisible }); 913 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 914 const toCancel = oldRangeToPrefetch.subtract(this.fetchedRegistry.rangeToFetch).toSet(); 915 this.cancel(toCancel); 916 } 917 prefetch(toPrefetch) { 918 if (this.isPaused) { 919 this.logger.debug('Prefetcher is paused. Do nothing.'); 920 return; 921 } 922 toPrefetch.forEach(this.singleFetch); 923 } 924 fetchedCallback(fetchId, prefetchStart) { 925 const itemIndex = this.fetches.getItem(fetchId); 926 this.fetches.deleteFetch(fetchId); 927 if (itemIndex === undefined) { 928 return; 929 } 930 this.prefetchRangeEvaluator.updateRangeToFetch({ 931 kind: 'item-fetched', 932 itemIndex, 933 fetchDuration: this.timeProvider.getCurrent() - prefetchStart, 934 }); 935 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 936 } 937 errorOnFetchCallback(fetchId, error) { 938 const itemIndex = this.fetches.getItem(fetchId); 939 if (itemIndex !== undefined) { 940 this.logger.warn(`failed to fetch item at ${itemIndex} ${JSON.stringify(error)}`); 941 } 942 this.fetches.deleteFetch(fetchId); 943 setTimeout(() => { 944 this.prefetch(this.fetchedRegistry.getItemsToFetch()); 945 }, this.afterErrorDelay); 946 } 947 cancel(toCancel) { 948 toCancel.forEach((itemIndex) => { 949 if (!this.fetches.isFetchingItem(itemIndex)) { 950 return; 951 } 952 this.fetches.deleteFetchByItem(itemIndex); 953 if (this.dataSource.cancel) { 954 this.logger.info('to cancel ' + itemIndex); 955 this.dataSource.cancel(itemIndex); 956 } 957 }); 958 } 959} 960const dummyLogger = { 961 debug: () => { }, 962 info: () => { }, 963 warn: () => { }, 964}; 965function reportError(logger, methodName, e) { 966 logger.warn(`Error in ${methodName}: ${e}\n${e.stack}`); 967} 968class IndexRange { 969 constructor(start, end) { 970 this.start = start; 971 this.end = end; 972 if (this.start > this.end) { 973 throw new Error('Invalid range'); 974 } 975 } 976 get length() { 977 return this.end - this.start; 978 } 979 toSet(target) { 980 const set = target ?? new Set(); 981 for (let i = this.start; i < this.end; ++i) { 982 set.add(i); 983 } 984 return set; 985 } 986 contains(value) { 987 if (typeof value === 'object') { 988 return this.start <= value.start && value.end <= this.end; 989 } 990 else { 991 return this.start <= value && value < this.end; 992 } 993 } 994 subtract(other) { 995 const result = new IndexRangeArray(); 996 if (other.start > this.start) { 997 result.push(new IndexRange(this.start, Math.min(this.end, other.start))); 998 } 999 if (other.end < this.end) { 1000 result.push(new IndexRange(Math.max(other.end, this.start), this.end)); 1001 } 1002 return result; 1003 } 1004 expandedWith(other) { 1005 return new IndexRange(Math.min(this.start, other.start), Math.max(this.end, other.end)); 1006 } 1007 forEachIndex(callback) { 1008 for (let i = this.start; i < this.end; ++i) { 1009 callback(i); 1010 } 1011 } 1012 equals(other) { 1013 return this.start === other.start && this.end === other.end; 1014 } 1015 toString() { 1016 return `[${this.start}, ${this.end})`; 1017 } 1018} 1019class IndexRangeArray extends Array { 1020 forEachIndex(callback) { 1021 this.forEach((range) => { 1022 range.forEachIndex(callback); 1023 }); 1024 } 1025 toSet() { 1026 const set = new Set(); 1027 this.forEach((range) => { 1028 range.toSet(set); 1029 }); 1030 return set; 1031 } 1032} 1033class RangeEdge { 1034 constructor(value, inclusive) { 1035 this.value = value; 1036 this.inclusive = inclusive; 1037 } 1038} 1039class RatioRange { 1040 constructor(start, end) { 1041 this.start = start; 1042 this.end = end; 1043 if (this.start.value > this.end.value) { 1044 throw new Error(`RatioRange: ${this.start.value} > ${this.end.value}`); 1045 } 1046 } 1047 static newEmpty() { 1048 return new RatioRange(new RangeEdge(0, false), new RangeEdge(0, false)); 1049 } 1050 contains(point) { 1051 if (point === this.start.value) { 1052 return this.start.inclusive; 1053 } 1054 if (point === this.end.value) { 1055 return this.end.inclusive; 1056 } 1057 return this.start.value < point && point < this.end.value; 1058 } 1059 toString() { 1060 return `${this.start.inclusive ? '[' : '('}${this.start.value}, ${this.end.value}${this.end.inclusive ? ']' : ')'}`; 1061 } 1062} 1063function assertNever(_) { 1064 throw _ + 'assertNever'; 1065} 1066 1067export default { BasicPrefetcher }; 1068