Skip to content

Commit

Permalink
Improve Storage Performance (#85)
Browse files Browse the repository at this point in the history
* Catch camera image when app is killed by Android. Fix #83.

* Improve storage page performance.

* Refresh proof list only on init.

* Avoid refreshing storage when adding tuples.

* Read file system only once on storage initialization.

* Generate and use thumbnail on Storage page.

* Use ion-img again for lazy-loading.

* Add notes for the potential bugs.
  • Loading branch information
seanwu1105 authored Sep 4, 2020
1 parent d96d8ee commit 9324237
Show file tree
Hide file tree
Showing 17 changed files with 194 additions and 70 deletions.
41 changes: 39 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@ionic/angular": "^5.3.2",
"@ionic/pwa-elements": "^3.0.1",
"@ngneat/transloco": "^2.19.1",
"image-blob-reduce": "^1.0.7",
"@ngneat/until-destroy": "^8.0.2",
"rxjs": "~6.6.2",
"tslib": "^2.0.1",
Expand Down
17 changes: 16 additions & 1 deletion src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Plugins } from '@capacitor/core';
import { Platform } from '@ionic/angular';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { map } from 'rxjs/operators';
import { CameraService } from './services/camera/camera.service';
import { CollectorService } from './services/collector/collector.service';
import { CapacitorProvider } from './services/collector/information/capacitor-provider/capacitor-provider';
import { DefaultSignatureProvider } from './services/collector/signature/default-provider/default-provider';
Expand All @@ -13,6 +15,7 @@ import { NotificationService } from './services/notification/notification.servic
import { PublishersAlert } from './services/publisher/publishers-alert/publishers-alert.service';
import { SamplePublisher } from './services/publisher/sample-publisher/sample-publisher';
import { SerializationService } from './services/serialization/serialization.service';
import { fromExtension } from './utils/mime-type';

const { SplashScreen } = Plugins;

Expand All @@ -32,14 +35,26 @@ export class AppComponent {
private readonly signatureRepository: SignatureRepository,
private readonly translocoService: TranslocoService,
private readonly notificationService: NotificationService,
langaugeService: LanguageService
langaugeService: LanguageService,
private readonly cameraService: CameraService
) {
this.restoreAppStatus();
this.initializeApp();
this.initializeCollector();
this.initializePublisher();
langaugeService.initialize$().pipe(untilDestroyed(this)).subscribe();
}

restoreAppStatus() {
this.cameraService.restoreKilledAppResult$().pipe(
map(cameraPhoto => this.collectorService.storeAndCollect(
cameraPhoto.base64String,
fromExtension(cameraPhoto.format)
)),
untilDestroyed(this)
).subscribe();
}

initializeApp() {
this.platform.ready().then(() => {
SplashScreen.hide();
Expand Down
11 changes: 2 additions & 9 deletions src/app/pages/information/information.page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { map, pluck, switchMap, switchMapTo } from 'rxjs/operators';
import { UntilDestroy } from '@ngneat/until-destroy';
import { map, pluck, switchMap } from 'rxjs/operators';
import { InformationType } from 'src/app/services/data/information/information';
import { InformationRepository } from 'src/app/services/data/information/information-repository.service';
import { ProofRepository } from 'src/app/services/data/proof/proof-repository.service';
Expand Down Expand Up @@ -44,11 +44,4 @@ export class InformationPage {
private readonly proofRepository: ProofRepository,
private readonly informationRepository: InformationRepository,
) { }

ionViewWillEnter() {
this.proofRepository.refresh$().pipe(
switchMapTo(this.informationRepository.refresh$()),
untilDestroyed(this)
).subscribe();
}
}
2 changes: 1 addition & 1 deletion src/app/pages/proof/proof.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</ion-header>

<ion-content *transloco="let t">
<ion-img [src]="'data:image/*;base64,' + (rawBase64$ | async)"></ion-img>
<img [src]="base64Src$ | async" />
<ion-list lines="none">
<ion-item>
<ion-icon slot="start" name="reader"></ion-icon>
Expand Down
14 changes: 4 additions & 10 deletions src/app/pages/proof/proof.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export class ProofPage {
isNonNullable()
);

readonly rawBase64$ = this.proof$.pipe(switchMap(proof => this.proofRepository.getRawFile$(proof)));
readonly base64Src$ = this.proof$.pipe(
switchMap(proof => this.proofRepository.getRawFile$(proof)),
map(rawBase64 => `data:image/png;base64,${rawBase64}`)
);
readonly hash$ = this.proof$.pipe(pluck('hash'));
readonly mimeType$ = this.proof$.pipe(pluck('mimeType'));
readonly timestamp$ = this.proof$.pipe(map(proof => new Date(proof.timestamp)));
Expand Down Expand Up @@ -73,15 +76,6 @@ export class ProofPage {
private readonly blockingActionService: BlockingActionService
) { }

ionViewWillEnter() {
this.proofRepository.refresh$().pipe(
switchMapTo(this.captionRepository.refresh$()),
switchMapTo(this.informationRepository.refresh$()),
switchMapTo(this.signatureRepository.refresh$()),
untilDestroyed(this)
).subscribe();
}

publish() {
this.proof$.pipe(
first(),
Expand Down
5 changes: 0 additions & 5 deletions src/app/pages/storage/storage.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@
</ion-header>

<ion-content>

<ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>

<ion-grid>
<ion-row>
<ion-col *ngFor="let proofWithRaw of proofsWithRaw$ | async"
Expand Down
17 changes: 2 additions & 15 deletions src/app/pages/storage/storage.page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { of, zip } from 'rxjs';
import { concatMap, map, mapTo } from 'rxjs/operators';
import { concatMap, map } from 'rxjs/operators';
import { CameraService } from 'src/app/services/camera/camera.service';
import { CollectorService } from 'src/app/services/collector/collector.service';
import { ProofRepository } from 'src/app/services/data/proof/proof-repository.service';
Expand All @@ -18,7 +18,7 @@ export class StoragePage {

private readonly proofs$ = this.proofRepository.getAll$();
readonly proofsWithRaw$ = this.proofs$.pipe(
concatMap(proofs => forkJoinWithDefault(proofs.map(proof => this.proofRepository.getRawFile$(proof)))),
concatMap(proofs => forkJoinWithDefault(proofs.map(proof => this.proofRepository.getThumbnail$(proof)))),
concatMap(base64Strings => zip(this.proofs$, of(base64Strings))),
map(([proofs, base64Strings]) => proofs.map((proof, index) => ({
proof,
Expand All @@ -32,19 +32,6 @@ export class StoragePage {
private readonly collectorService: CollectorService
) { }

ionViewWillEnter() {
this.proofRepository.refresh$().pipe(
untilDestroyed(this)
).subscribe();
}

refresh(event: any) {
this.proofRepository.refresh$().pipe(
mapTo(event.target.complete()),
untilDestroyed(this)
).subscribe();
}

capture() {
this.cameraService.capture$().pipe(
map(cameraPhoto => this.collectorService.storeAndCollect(
Expand Down
23 changes: 19 additions & 4 deletions src/app/services/camera/camera.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import { CameraResultType, CameraSource, Plugins } from '@capacitor/core';
import { defer } from 'rxjs';
import { map } from 'rxjs/operators';
import { AppRestoredResult, CameraPhoto, CameraResultType, CameraSource, Plugins } from '@capacitor/core';
import { defer, fromEventPattern } from 'rxjs';
import { filter, map } from 'rxjs/operators';

const { Camera } = Plugins;
const { App, Camera } = Plugins;

@Injectable({
providedIn: 'root'
Expand All @@ -24,4 +24,19 @@ export class CameraService {
}))
);
}

restoreKilledAppResult$() {
const appRestored$ = fromEventPattern<AppRestoredResult>(
handler => App.addListener('appRestoredResult', handler)
);
return appRestored$.pipe(
filter(result => result.pluginId === 'Camera' && result.methodName === 'getPhoto' && result.success),
map(result => result.data as CameraPhoto),
map(cameraPhoto => ({
format: cameraPhoto.format,
// tslint:disable-next-line: no-non-null-assertion
base64String: cameraPhoto.base64String!
}))
);
}
}
2 changes: 0 additions & 2 deletions src/app/services/data/caption/caption-repository.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ export class CaptionRepository {

private readonly captionStorage = new Storage<Caption>('caption');

refresh$() { return this.captionStorage.refresh$(); }

getByProof$(proof: Proof) {
return this.captionStorage.getAll$().pipe(
map(captions => captions.find(caption => caption.proofHash === proof.hash))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export class InformationRepository {

private readonly informationStorage = new Storage<Information>('information');

refresh$() { return this.informationStorage.refresh$(); }

getByProof$(proof: Proof) {
return this.informationStorage.getAll$().pipe(
map(informationList => informationList.filter(info => info.proofHash === proof.hash))
Expand Down
45 changes: 39 additions & 6 deletions src/app/services/data/proof/proof-repository.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { FilesystemDirectory, Plugins } from '@capacitor/core';
import { defer } from 'rxjs';
import { filter, map, pluck, switchMap, switchMapTo } from 'rxjs/operators';
import { defer, zip } from 'rxjs';
import { map, pluck, switchMap, switchMapTo } from 'rxjs/operators';
import { sha256WithBase64$ } from 'src/app/utils/crypto/crypto';
import { base64ToBlob$, blobToBase64$ } from 'src/app/utils/encoding/encoding';
import { getExtension, MimeType } from 'src/app/utils/mime-type';
import { forkJoinWithDefault } from 'src/app/utils/rx-operators';
import { Storage } from 'src/app/utils/storage/storage';
Expand All @@ -12,6 +13,8 @@ import { SignatureRepository } from '../signature/signature-repository.service';
import { Proof } from './proof';

const { Filesystem } = Plugins;
// @ts-ignore
const ImageBlobReduce = require('image-blob-reduce')();

@Injectable({
providedIn: 'root'
Expand All @@ -21,21 +24,21 @@ export class ProofRepository {
private readonly proofStorage = new Storage<Proof>('proof');
private readonly rawFileDir = FilesystemDirectory.Data;
private readonly rawFileFolderName = 'raw';
private readonly thumbnailFileDir = FilesystemDirectory.Data;
private readonly thumbnailFileFolderName = 'thumb';
private readonly thumbnailSize = 200;

constructor(
private readonly captionRepository: CaptionRepository,
private readonly informationRepository: InformationRepository,
private readonly signatureRepository: SignatureRepository
) { }

refresh$() { return this.proofStorage.refresh$(); }

getAll$() { return this.proofStorage.getAll$(); }

getByHash$(hash: string) {
return this.getAll$().pipe(
map(proofList => proofList.find(proof => proof.hash === hash)),
filter(proof => !!proof)
map(proofList => proofList.find(proof => proof.hash === hash))
);
}

Expand Down Expand Up @@ -63,6 +66,15 @@ export class ProofRepository {
* @param mimeType The file added in the internal storage. The name of the returned file will be its hash with original extension.
*/
saveRawFile$(rawBase64: string, mimeType: MimeType) {
return zip(
this._saveRawFile$(rawBase64, mimeType),
this.generateAndSaveThumbnailFile$(rawBase64, mimeType)
).pipe(
map(([rawUri, _]) => rawUri)
);
}

private _saveRawFile$(rawBase64: string, mimeType: MimeType) {
return sha256WithBase64$(rawBase64).pipe(
switchMap(hash => Filesystem.writeFile({
path: `${this.rawFileFolderName}/${hash}.${getExtension(mimeType)}`,
Expand All @@ -80,4 +92,25 @@ export class ProofRepository {
directory: this.rawFileDir
}));
}

getThumbnail$(proof: Proof) {
return defer(() => Filesystem.readFile({
path: `${this.thumbnailFileFolderName}/${proof.hash}.${getExtension(proof.mimeType)}`,
directory: this.thumbnailFileDir
})).pipe(pluck('data'));
}

private generateAndSaveThumbnailFile$(rawBase64: string, mimeType: MimeType) {
return base64ToBlob$(`data:${mimeType};base64,${rawBase64}`).pipe(
switchMap(rawImageBlob => ImageBlobReduce.toBlob(rawImageBlob, { max: this.thumbnailSize })),
switchMap(thumbnailBlob => zip(blobToBase64$(thumbnailBlob as Blob), sha256WithBase64$(rawBase64))),
switchMap(([thumbnailBase64, hash]) => Filesystem.writeFile({
path: `${this.thumbnailFileFolderName}/${hash}.${getExtension(mimeType)}`,
data: thumbnailBase64,
directory: this.thumbnailFileDir,
recursive: true
})),
pluck('uri')
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export class SignatureRepository {

private readonly signatureStorage = new Storage<Signature>('signature');

refresh$() { return this.signatureStorage.refresh$(); }

getByProof$(proof: Proof) {
return this.signatureStorage.getAll$().pipe(
map(signatures => signatures.filter(info => info.proofHash === proof.hash))
Expand Down
Loading

0 comments on commit 9324237

Please sign in to comment.