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