# network-prefetch-demo
**Repository Path**: Hyperb0rean/network-prefetch-demo
## Basic Information
- **Project Name**: network-prefetch-demo
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 3
- **Created**: 2024-05-14
- **Last Updated**: 2024-05-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
ArkUI PoC demo app for images prefetching
## 1. Preamble
To improve scrolling experience and minimize number of white blocks and their on-screen time, currently the only mechanism provided by AkrUI API is `cachedCount` parameter, which can be applied to ``, `` and `` components. This parameter sets the number of items pre-loaded and pre-rendered outside of the screen area, like in the code snippet below:
```typescript
List() {
LazyForEach(this.dataSource, () => { ListItem() { } })
}
.cachedCount(20)
```
To further improve user experience and provide app developer with additional abilities to prefetch data items the approach developed by RRI OS Networking team can be used. Used entities and interfaces are described below. The details of the proposed `IPrefetcher` implementation `` and reasoning behind it can be found in accompanying presentation **"White blocks problem solution"**. According to the measurements performed `` can give a noticeable gain in scrolling experience and CPU savings in comparison with conventional `cachedCount` parametrization.
## 2. Used entities and interfaces
The responsibility to prefetch data items from network before they get appeared on the screen is divided into two parts. First, it is supposed that dataSource is capable of downloading required data by itself and hence can serve as a data cache, storing and managing data items, like images, either in memory or in file system. For that purpose app developer should implement `IDataSourcePrefetching` interface, in particular its `prefetch()` and `cancel()` methods:
```typescript
interface IDataSourcePrefetching extends IDataSource {
prefetch(index: number): Promise | void; // prefetches the data item that matches the specified index
cancel?(index: number): Promise | void; // cancels prefething / downloading requests for the specified data item
};
```
It is highly recommended to use **'rcp'** high-performance HTTP engine from **'@hms.collaboration.rcp'** to prefetch data via HTTP(S) protocols. So the implementation could look like:
```typescript
import rcp from '@hms.collaboration.rcp';
...
export class DataSourcePrefetchingRCP implements IDataSourcePrefetching {
private data: Array;
private session: rcp.Session;
private requestsInFlight: HashMap;
private requestsCancelled: HashMap;
...
public async prefetch(index: number) {
const item = this.data[index];
const handleError = (message: string) => {
logger.error(message);
item.cachedImage = IMAGE_UNAVAILABLE;
}
if (item.cachedImage === undefined || this.requestsInFlight.hasKey(index)) {
return Promise.reject('Already being prefetched');
}
if (item.cachedImage !== null) {
// Already prefetched
// We resolve this case so we can rely on resolution meaning the item has been cached
return Promise.resolve();
}
const request = new rcp.Request(item.albumUrl, 'GET');
this.requestsInFlight.set(index, request);
let response: rcp.Response;
try {
response = await this.session.fetch(request);
} catch (reason) {
this.requestsInFlight.remove(index);
const CANCEL_CODE: number = 1007900992; // As defined by RCP
if (reason.code === CANCEL_CODE) {
this.requestsCancelled.remove(index);
} else {
handleError(`Non-cancel rejection from RCP: ${reason.code}`);
}
return Promise.reject();
}
if (response.statusCode !== 200) {
handleError(`Got status ${response.statusCode} for ${item.songId} / ${item.albumUrl}`);
return Promise.reject();
}
if (this.requestsCancelled.hasKey(index)) {
this.requestsCancelled.remove(index);
}
item.cachedImage = undefined;
this.requestsInFlight.remove(index);
if (response.body === undefined) {
handleError(`Got empty response for ${item.songId} / ${item.albumUrl}`);
return Promise.reject();
}
try {
item.cachedImage = await this.cache(item.songId, response.body);
} catch (reason) {
handleError(`Failed to cache image to disk: ${reason}`);
return Promise.reject();
}
return Promise.resolve();
}
private async cache(songId: number, data: ArrayBuffer): Promise {
const path = `file://${this.cachePath}/${songId}.jpg`;
let file = await fs.open(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
await fs.write(file.fd, data);
await fs.close(file);
return Promise.resolve(path);
}
public async cancel(index: number) {
if (this.requestsInFlight.hasKey(index) && !this.requestsCancelled.hasKey(index)) {
const request = this.requestsInFlight.get(index);
this.requestsCancelled.set(index, request);
this.session.cancel(request);
}
}
...
}
```
Full implementation of `DataSourcePrefetchingRCP` class can be found in [DataSourcePrefetchingRCP.ets](https://rnd-gitlab-msc.huawei.com/rus-os-team/osalgorithms/network/network-api/openharmony-demo-applications/arkui-list-preload-images/-/blob/active-data-source/entry/src/main/ets/viewmodel/DataSourcePrefetchingRCP.ets) file.
In the approach described it is implied that dataSource should not return data item's URLs in any way, but instead it should return file path to the cached data when it has been successfully prefetched or null value otherwise. In the example above it means that `item.cachedImagePath` should be provided to `` component instead of providing `item.imageUrl`:
```typescript
@Component
struct ListItemComponent {
@ObjectLink private item: dataItem;
build() {
Row() {
Image(this.item.cachedImagePath ?? $r('app.media.default'))
}
}
}
```
```typescript
@Observed
class dataItem {
...
imageUrl: string;
cachedImagePath?: ResourceStr;
...
}
```
Please note `@Observed` and `@ObjectLink` decorators used for data item to trigger UI update when `cachedImagePath` gets filled in by the dataSource.
It is _not_ supposed to provide app developer with _any_ kind of dataSource implementation, since it is application specific and is highly coupled with app business domain, but instead publish `DataSourcePrefetchingRCP` implementation in documentation as an example and best practice for building high performance scrolling experience for ``, `` and `` components.
The second part of the solution described is implemenation of `IPrefetcher` interface which is supposed to make conscious decisions about what data items to prefetch and what prefetching requests to cancel based on the realtime changes of visible on-screen area due to scrolling events and variety of prefetching response times caused by changing networking conditions. In other words, that is `IPrefetcher` implemenation which drives dataSource prefetch and cancellation execution.
```typescript
export interface IPrefetcher {
setDataSource(ds: IDataSourcePrefetching): void; // sets dataSource instance satisfying the requirements above
visibleAreaChanged(minVisible: number, maxVisible: number): void; // updates visible area boundaries
};
```
As part of the solution it is supposed that ArkUI will be shipped with `BasicScrollingPrefetcher` implementation developed by RRI OS Networking team and which is generic enough to outperform any static `cachedCount` value, so that it can be re-used by app developers in similar scenarios. The internals of `BasicScrollingPrefetcher` and the details of how it works can be found in accompanying presentation **"White blocks problem solution"**.
Full implementation of `BasicScrollingPrefetcher` class can be found in [BasicScrollingPrefetcher.ets](https://rnd-gitlab-msc.huawei.com/rus-os-team/osalgorithms/network/network-api/openharmony-demo-applications/arkui-list-preload-images/-/blob/active-data-source/entry/active_datasource/src/BasicScrollingPrefetcher.ets) file.
## 3. Possible API extensions for ``
Current implemenation does not imply any extensions of ArkUI API as app developer may use `onScrollIndex()` callback to inform `IPrefetcher` implemenation about visible area changes as it is shown in the example below:
```typescript
@Component
struct MainComponent {
...
dataSource = new DataSourcePrefetchingRCP();
prefetcher = new BasicScrollingPrefetcher();
this.prefetcher.setDataSource(this.dataSource);
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: dataItem) => {
ListItem() {
ListItemComponent( { item: item } )
}
}, (item: dataItem) => item.itemId.toString())
}
.cachedCount(5)
.onScrollIndex((start, end) => {
this.prefetcher.visibleAreaChanged(start, end);
})
}
}
}
```
However, passing visible area boundaries around seems excessive and it could make sense to implement such notification right within `LazyForEach`. In such case and provided that the default `IPrefetcher` implementation is shipped in ArkTS layer of ArkUI (hopefully, `BasicScrollingPrefetcher` will become such default implemenation) it will become sufficient to introduce just one additional method `setPrefetcher({} as IPrefetcher)` to `LazyForEach` having the following behaviour:
- if no instance of `IPrefetcher` is provided to `setPrefetcher()` then prefetching is disabled
- if any instance of `IPrefetcher` is provided to `setPrefetcher()` then prefetching is enabled and instance of `IDataSourcePrefetching` should be passed to `` as dataSource; otherwise code should throw or not compile
- if prefetching is enabled then "on-scroll" events should be handled internally and instance of `IPrefetcher` should be notified by calling its `visibleAreaChanged(number, number)` method
So the code could become as follows:
```typescript
@Component
struct MainComponent {
...
dataSource = new DataSourcePrefetchingRCP();
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: dataItem) => {
ListItem() {
ListItemComponent( { item: item } )
}
}, (item: dataItem) => item.itemId.toString())
.setPrefetcher(new BasicScrollingPrefetcher(this.dataSource)) // enables prefetching and sets IPrefetcher implemenation
}
.cachedCount(5)
}
}
}
```
Another option would be to implement the logic of `BasicScrollingPrefetcher` in native code and neither expose it via ArkTS nor ship it with ArkTS layer of ArkUI. In such case it would be necessary to introduce two methods to `LazyForEach` instead of just one: namely, `enablePrefetching()` to explicitly enable prefetching and `setCustomPrefetcher(/*IPrefetcher implementation*/)` to override default prefetching behaviour.
The API with `setCustomPrefetcher()` and `enablePrefetching()` is a better choice comparing to the API with just single `setPrefetcher()` method even if `BasicScrollingPrefetcher` is located in ArkTS code. The reason is that app developer could enable prefetching with just simple `enablePrefetching()` call instead of the `setPrefetching()` call with complex prefetcher instantiation.
```typescript
@Component
struct MainComponent {
...
dataSource = new DataSourcePrefetchingRCP();
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: dataItem) => {
ListItem() {
ListItemComponent( { item: item } )
}
}, (item: dataItem) => item.itemId.toString())
.enablePrefetching() // enables prefetching if dataSource provided is of type IDataSourcePrefetching
.setCustomPrefetcher(new MyFavoritePrefetcher(this.dataSource)) // sets custom Prefetcher
}
.cachedCount(5)
}
}
}
```
The example above assumes that just setting custom prefetcher via `setCustomPrefetcher()` doesn't automatically enable prefetching: explicit call of `enablePrefetching()` is required anyway.
Also, it's worth noting that introducing `setPrefetcher()` or `enablePrefetching()` and `setCustomPrefetcher()` for `` seems more natural. Due to some reasons currently `cachedCount` is assigned to containers like ``, `` and ``, so API extension of these containers can also be considered instead of `` but it seems a bit odd and should be discussed separately.