Merge pull request #3070 from NoelDeMartin/MOBILE-3971
MOBILE-3971: Improve config db performance
This commit is contained in:
		
						commit
						e523272e5e
					
				| @ -40,6 +40,7 @@ for (const file of files) { | ||||
|         styling: [], | ||||
|         blocking: [], | ||||
|         longTasks: [], | ||||
|         database: [], | ||||
|         networking: [], | ||||
|     }; | ||||
|     performanceMeasures[performanceMeasure.name].duration.push(performanceMeasure.duration); | ||||
| @ -47,17 +48,19 @@ for (const file of files) { | ||||
|     performanceMeasures[performanceMeasure.name].styling.push(performanceMeasure.styling); | ||||
|     performanceMeasures[performanceMeasure.name].blocking.push(performanceMeasure.blocking); | ||||
|     performanceMeasures[performanceMeasure.name].longTasks.push(performanceMeasure.longTasks); | ||||
|     performanceMeasures[performanceMeasure.name].database.push(performanceMeasure.database); | ||||
|     performanceMeasures[performanceMeasure.name].networking.push(performanceMeasure.networking); | ||||
| } | ||||
| 
 | ||||
| // Calculate averages
 | ||||
| for (const [name, { duration, scripting, styling, blocking, longTasks, networking }] of Object.entries(performanceMeasures)) { | ||||
| for (const [name, { duration, scripting, styling, blocking, longTasks, database, networking }] of Object.entries(performanceMeasures)) { | ||||
|     const totalRuns = duration.length; | ||||
|     const averageDuration = Math.round(duration.reduce((total, duration) => total + duration) / totalRuns); | ||||
|     const averageScripting = Math.round(scripting.reduce((total, scripting) => total + scripting) / totalRuns); | ||||
|     const averageStyling = Math.round(styling.reduce((total, styling) => total + styling) / totalRuns); | ||||
|     const averageBlocking = Math.round(blocking.reduce((total, blocking) => total + blocking) / totalRuns); | ||||
|     const averageLongTasks = Math.round(longTasks.reduce((total, longTasks) => total + longTasks) / totalRuns); | ||||
|     const averageDatabase = Math.round(database.reduce((total, database) => total + database) / totalRuns); | ||||
|     const averageNetworking = Math.round(networking.reduce((total, networking) => total + networking) / totalRuns); | ||||
| 
 | ||||
|     performanceMeasures[name] = { | ||||
| @ -66,6 +69,7 @@ for (const [name, { duration, scripting, styling, blocking, longTasks, networkin | ||||
|         'Styling': `${averageStyling}ms`, | ||||
|         'Blocking': `${averageBlocking}ms`, | ||||
|         '# Network requests': averageNetworking, | ||||
|         '# DB Queries': averageDatabase, | ||||
|         '# Long Tasks': averageLongTasks, | ||||
|         '# runs': totalRuns, | ||||
|     }; | ||||
|  | ||||
							
								
								
									
										194
									
								
								src/core/classes/database-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								src/core/classes/database-table.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,194 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { SQLiteDB, SQLiteDBRecordValues } from './sqlitedb'; | ||||
| 
 | ||||
| /** | ||||
|  * Database table wrapper used to improve performance by caching all data in memory | ||||
|  * for faster read operations. | ||||
|  */ | ||||
| export abstract class CoreDatabaseTable< | ||||
|     DBRecord extends SQLiteDBRecordValues = SQLiteDBRecordValues, | ||||
|     PrimaryKeyColumns extends keyof DBRecord = 'id', | ||||
|     PrimaryKey extends GetPrimaryKey<DBRecord, PrimaryKeyColumns> = GetPrimaryKey<DBRecord, PrimaryKeyColumns> | ||||
| > { | ||||
| 
 | ||||
|     /** | ||||
|      * Create an instance. | ||||
|      * | ||||
|      * @param db Database connection. | ||||
|      * @returns Instance. | ||||
|      */ | ||||
|     static async create<This extends AnyCoreDatabaseTable>(this: CoreDatabaseTableConstructor<This>, db: SQLiteDB): Promise<This> { | ||||
|         const instance = new this(db); | ||||
| 
 | ||||
|         await instance.initialize(); | ||||
| 
 | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     protected db: SQLiteDB; | ||||
|     protected data: Record<string, DBRecord>; | ||||
|     protected primaryKeys: string[] = ['id']; | ||||
| 
 | ||||
|     constructor(db: SQLiteDB) { | ||||
|         this.db = db; | ||||
|         this.data = {}; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Find a record matching the given conditions. | ||||
|      * | ||||
|      * @param conditions Matching conditions. | ||||
|      * @returns Database record. | ||||
|      */ | ||||
|     find(conditions: Partial<DBRecord>): DBRecord | null  { | ||||
|         return Object.values(this.data).find(record => this.recordMatches(record, conditions)) ?? null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Find a record by its primary key. | ||||
|      * | ||||
|      * @param primaryKey Primary key. | ||||
|      * @returns Database record. | ||||
|      */ | ||||
|     findByPrimaryKey(primaryKey: PrimaryKey): DBRecord | null { | ||||
|         return this.data[this.serializePrimaryKey(primaryKey)] ?? null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Insert a new record. | ||||
|      * | ||||
|      * @param record Database record. | ||||
|      */ | ||||
|     async insert(record: DBRecord): Promise<void> { | ||||
|         await this.db.insertRecord(this.table, record); | ||||
| 
 | ||||
|         const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record)); | ||||
| 
 | ||||
|         this.data[primaryKey] = record; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete records matching the given conditions. | ||||
|      * | ||||
|      * @param conditions Matching conditions. If this argument is missing, all records will be deleted. | ||||
|      */ | ||||
|     async delete(conditions?: Partial<DBRecord>): Promise<void> { | ||||
|         if (!conditions) { | ||||
|             await this.db.deleteRecords(this.table); | ||||
| 
 | ||||
|             this.data = {}; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await this.db.deleteRecords(this.table, conditions); | ||||
| 
 | ||||
|         Object.entries(this.data).forEach(([id, record]) => { | ||||
|             if (!this.recordMatches(record, conditions)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             delete this.data[id]; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a single record identified by its primary key. | ||||
|      * | ||||
|      * @param primaryKey Record primary key. | ||||
|      */ | ||||
|     async deleteByPrimaryKey(primaryKey: PrimaryKey): Promise<void> { | ||||
|         await this.db.deleteRecords(this.table, primaryKey); | ||||
| 
 | ||||
|         delete this.data[this.serializePrimaryKey(primaryKey)]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Database table name. | ||||
|      */ | ||||
|     protected abstract get table(): string; | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize object by getting the current state of the database table. | ||||
|      */ | ||||
|     protected async initialize(): Promise<void> { | ||||
|         const records = await this.db.getRecords<DBRecord>(this.table); | ||||
| 
 | ||||
|         this.data = records.reduce((data, record) => { | ||||
|             const primaryKey = this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record)); | ||||
| 
 | ||||
|             data[primaryKey] = record; | ||||
| 
 | ||||
|             return data; | ||||
|         }, {}); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an object with the columns representing the primary key of a database record. | ||||
|      * | ||||
|      * @param record Database record. | ||||
|      * @returns Primary key column-value pairs. | ||||
|      */ | ||||
|     protected getPrimaryKeyFromRecord(record: DBRecord): PrimaryKey { | ||||
|         return this.primaryKeys.reduce((primaryKey, column) => { | ||||
|             primaryKey[column] = record[column]; | ||||
| 
 | ||||
|             return primaryKey; | ||||
|         }, {} as Record<PrimaryKeyColumns, unknown>) as PrimaryKey; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Serialize a primary key with a string representation. | ||||
|      * | ||||
|      * @param primaryKey Database record primary key. | ||||
|      * @returns Serialized primary key. | ||||
|      */ | ||||
|     protected serializePrimaryKey(primaryKey: PrimaryKey): string { | ||||
|         return Object.values(primaryKey).map(value => String(value)).join('-'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether a given record matches the given conditions. | ||||
|      * | ||||
|      * @param record Database record. | ||||
|      * @param conditions Conditions. | ||||
|      * @returns Whether the record matches the conditions. | ||||
|      */ | ||||
|     protected recordMatches(record: DBRecord, conditions: Partial<DBRecord>): boolean { | ||||
|         return !Object.entries(conditions).some(([column, value]) => record[column] !== value); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Generic type to match against any concrete database table type. | ||||
|  */ | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
| type AnyCoreDatabaseTable = CoreDatabaseTable<SQLiteDBRecordValues, string, Record<string, any>>; | ||||
| 
 | ||||
| /** | ||||
|  * Database table constructor. | ||||
|  */ | ||||
| type CoreDatabaseTableConstructor<T extends AnyCoreDatabaseTable> = { | ||||
|     new (db: SQLiteDB): T; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Infer primary key type from database record and columns types. | ||||
|  */ | ||||
| type GetPrimaryKey<DBRecord extends SQLiteDBRecordValues, PrimaryKeyColumns extends keyof DBRecord> = { | ||||
|     [column in PrimaryKeyColumns]: DBRecord[column]; | ||||
| }; | ||||
| @ -16,6 +16,7 @@ import { SQLiteObject } from '@ionic-native/sqlite/ngx'; | ||||
| 
 | ||||
| import { SQLite, Platform } from '@singletons'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| import { CoreDB } from '@services/db'; | ||||
| 
 | ||||
| type SQLiteDBColumnType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB'; | ||||
| 
 | ||||
| @ -813,12 +814,15 @@ export class SQLiteDB { | ||||
|      * Initialize the database. | ||||
|      */ | ||||
|     init(): void { | ||||
|         this.promise = Platform.ready() | ||||
|             .then(() => SQLite.create({ | ||||
|                 name: this.name, | ||||
|                 location: 'default', | ||||
|             })) | ||||
|             .then((db: SQLiteObject) => { | ||||
|         this.promise = this.createDatabase().then(db => { | ||||
|             if (CoreDB.loggingEnabled()) { | ||||
|                 const spies = this.getDatabaseSpies(db); | ||||
| 
 | ||||
|                 db = new Proxy(db, { | ||||
|                     get: (target, property, receiver) => spies[property] ?? Reflect.get(target, property, receiver), | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             this.db = db; | ||||
| 
 | ||||
|             return; | ||||
| @ -1147,6 +1151,50 @@ export class SQLiteDB { | ||||
|         return { sql, params }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open a database connection. | ||||
|      * | ||||
|      * @returns Database. | ||||
|      */ | ||||
|     protected async createDatabase(): Promise<SQLiteObject> { | ||||
|         await Platform.ready(); | ||||
| 
 | ||||
|         return SQLite.create({ name: this.name, location: 'default' }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get database spy methods to intercept database calls and track logging information. | ||||
|      * | ||||
|      * @param db Database to spy. | ||||
|      * @returns Spy methods. | ||||
|      */ | ||||
|     protected getDatabaseSpies(db: SQLiteObject): Partial<SQLiteObject> { | ||||
|         return { | ||||
|             executeSql(statement, params) { | ||||
|                 const start = performance.now(); | ||||
| 
 | ||||
|                 return db.executeSql(statement, params).then(result => { | ||||
|                     CoreDB.logQuery(statement, performance.now() - start, params); | ||||
| 
 | ||||
|                     return result; | ||||
|                 }); | ||||
|             }, | ||||
|             sqlBatch(statements) { | ||||
|                 const start = performance.now(); | ||||
| 
 | ||||
|                 return db.sqlBatch(statements).then(result => { | ||||
|                     const sql = Array.isArray(statements) | ||||
|                         ? statements.join(' | ') | ||||
|                         : String(statements); | ||||
| 
 | ||||
|                     CoreDB.logQuery(sql, performance.now() - start); | ||||
| 
 | ||||
|                     return result; | ||||
|                 }); | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export type SQLiteDBRecordValues = { | ||||
|  | ||||
							
								
								
									
										124
									
								
								src/core/classes/tests/database-table.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/core/classes/tests/database-table.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { mock } from '@/testing/utils'; | ||||
| import { CoreDatabaseTable } from '@classes/database-table'; | ||||
| import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; | ||||
| 
 | ||||
| interface User extends SQLiteDBRecordValues { | ||||
|     id: number; | ||||
|     name: string; | ||||
|     surname: string; | ||||
| } | ||||
| 
 | ||||
| class UsersTable extends CoreDatabaseTable<User> { | ||||
| 
 | ||||
|     protected table = 'users'; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| describe('CoreDatabaseTable', () => { | ||||
| 
 | ||||
|     let records: User[]; | ||||
|     let db: SQLiteDB; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         records = []; | ||||
|         db = mock<SQLiteDB>({ | ||||
|             getRecords: async <T>() => records as unknown as T[], | ||||
|             deleteRecords: async () => 0, | ||||
|             insertRecord: async () => 0, | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('reads all records on create', async () => { | ||||
|         await UsersTable.create(db); | ||||
| 
 | ||||
|         expect(db.getRecords).toHaveBeenCalledWith('users'); | ||||
|     }); | ||||
| 
 | ||||
|     it('finds items', async () => { | ||||
|         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||
|         const amy = { id: 2, name: 'Amy', surname: 'Doe' }; | ||||
| 
 | ||||
|         records.push(john); | ||||
|         records.push(amy); | ||||
| 
 | ||||
|         const table = await UsersTable.create(db); | ||||
| 
 | ||||
|         expect(table.findByPrimaryKey({ id: 1 })).toEqual(john); | ||||
|         expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy); | ||||
|         expect(table.find({ surname: 'Doe', name: 'John' })).toEqual(john); | ||||
|         expect(table.find({ surname: 'Doe', name: 'Amy' })).toEqual(amy); | ||||
|     }); | ||||
| 
 | ||||
|     it('inserts items', async () => { | ||||
|         // Arrange.
 | ||||
|         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||
| 
 | ||||
|         // Act.
 | ||||
|         const table = await UsersTable.create(db); | ||||
| 
 | ||||
|         await table.insert(john); | ||||
| 
 | ||||
|         // Assert.
 | ||||
|         expect(db.insertRecord).toHaveBeenCalledWith('users', john); | ||||
| 
 | ||||
|         expect(table.findByPrimaryKey({ id: 1 })).toEqual(john); | ||||
|     }); | ||||
| 
 | ||||
|     it('deletes items', async () => { | ||||
|         // Arrange.
 | ||||
|         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||
|         const amy = { id: 2, name: 'Amy', surname: 'Doe' }; | ||||
|         const jane = { id: 3, name: 'Jane', surname: 'Smith' }; | ||||
| 
 | ||||
|         records.push(john); | ||||
|         records.push(amy); | ||||
|         records.push(jane); | ||||
| 
 | ||||
|         // Act.
 | ||||
|         const table = await UsersTable.create(db); | ||||
| 
 | ||||
|         await table.delete({ surname: 'Doe' }); | ||||
| 
 | ||||
|         // Assert.
 | ||||
|         expect(db.deleteRecords).toHaveBeenCalledWith('users', { surname: 'Doe' }); | ||||
| 
 | ||||
|         expect(table.findByPrimaryKey({ id: 1 })).toBeNull(); | ||||
|         expect(table.findByPrimaryKey({ id: 2 })).toBeNull(); | ||||
|         expect(table.findByPrimaryKey({ id: 3 })).toEqual(jane); | ||||
|     }); | ||||
| 
 | ||||
|     it('deletes items by primary key', async () => { | ||||
|         // Arrange.
 | ||||
|         const john = { id: 1, name: 'John', surname: 'Doe' }; | ||||
|         const amy = { id: 2, name: 'Amy', surname: 'Doe' }; | ||||
| 
 | ||||
|         records.push(john); | ||||
|         records.push(amy); | ||||
| 
 | ||||
|         // Act.
 | ||||
|         const table = await UsersTable.create(db); | ||||
| 
 | ||||
|         await table.deleteByPrimaryKey({ id: 1 }); | ||||
| 
 | ||||
|         // Assert.
 | ||||
|         expect(db.deleteRecords).toHaveBeenCalledWith('users', { id: 1 }); | ||||
| 
 | ||||
|         expect(table.findByPrimaryKey({ id: 1 })).toBeNull(); | ||||
|         expect(table.findByPrimaryKey({ id: 2 })).toEqual(amy); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
| @ -15,6 +15,8 @@ | ||||
| /* tslint:disable:no-console */ | ||||
| 
 | ||||
| import { SQLiteDB } from '@classes/sqlitedb'; | ||||
| import { DbTransaction, SQLiteObject } from '@ionic-native/sqlite/ngx'; | ||||
| import { CoreDB } from '@services/db'; | ||||
| 
 | ||||
| /** | ||||
|  * Class to mock the interaction with the SQLite database. | ||||
| @ -158,16 +160,6 @@ export class SQLiteDBMock extends SQLiteDB { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the database. | ||||
|      */ | ||||
|     init(): void { | ||||
|         // This DB is for desktop apps, so use a big size to be sure it isn't filled.
 | ||||
|         // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|         this.db = (<any> window).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024); | ||||
|         this.promise = Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Open the database. Only needed if it was closed before, a database is automatically opened when created. | ||||
|      * | ||||
| @ -178,4 +170,40 @@ export class SQLiteDBMock extends SQLiteDB { | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async createDatabase(): Promise<SQLiteObject> { | ||||
|         // This DB is for desktop apps, so use a big size to be sure it isn't filled.
 | ||||
|         return (window as unknown as WebSQLWindow).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getDatabaseSpies(db: SQLiteObject): Partial<SQLiteObject> { | ||||
|         return { | ||||
|             transaction: (callback) => db.transaction((transaction) => { | ||||
|                 const transactionSpy: DbTransaction = { | ||||
|                     executeSql(sql, params, success, error) { | ||||
|                         const start = performance.now(); | ||||
|                         const resolve = callback => (...args) => { | ||||
|                             CoreDB.logQuery(sql, performance.now() - start, params); | ||||
| 
 | ||||
|                             return callback(...args); | ||||
|                         }; | ||||
| 
 | ||||
|                         return transaction.executeSql(sql, params, resolve(success), resolve(error)); | ||||
|                     }, | ||||
|                 }; | ||||
| 
 | ||||
|                 return callback(transactionSpy); | ||||
|             }), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| interface WebSQLWindow extends Window { | ||||
|     openDatabase(name: string, version: string, displayName: string, estimatedSize: number): SQLiteObject; | ||||
| } | ||||
|  | ||||
| @ -16,12 +16,14 @@ import { ApplicationRef, NgZone as NgZoneService } from '@angular/core'; | ||||
| import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications'; | ||||
| import { CoreApp, CoreAppProvider } from '@services/app'; | ||||
| import { CoreCronDelegate, CoreCronDelegateService } from '@services/cron'; | ||||
| import { CoreDB, CoreDbProvider } from '@services/db'; | ||||
| import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes'; | ||||
| import { Application, NgZone } from '@singletons'; | ||||
| 
 | ||||
| type AutomatedTestsWindow = Window & { | ||||
|     appRef?: ApplicationRef; | ||||
|     appProvider?: CoreAppProvider; | ||||
|     dbProvider?: CoreDbProvider; | ||||
|     cronProvider?: CoreCronDelegateService; | ||||
|     ngZone?: NgZoneService; | ||||
|     pushNotifications?: CorePushNotificationsProvider; | ||||
| @ -31,6 +33,7 @@ type AutomatedTestsWindow = Window & { | ||||
| function initializeAutomatedTestsWindow(window: AutomatedTestsWindow) { | ||||
|     window.appRef = Application.instance; | ||||
|     window.appProvider = CoreApp.instance; | ||||
|     window.dbProvider = CoreDB.instance; | ||||
|     window.cronProvider = CoreCronDelegate.instance; | ||||
|     window.ngZone = NgZone.instance; | ||||
|     window.pushNotifications = CorePushNotifications.instance; | ||||
|  | ||||
| @ -15,9 +15,10 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { SQLiteDB } from '@classes/sqlitedb'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database/config'; | ||||
| import { CoreDatabaseTable } from '@classes/database-table'; | ||||
| import { CorePromisedValue } from '@classes/promised-value'; | ||||
| 
 | ||||
| /** | ||||
|  * Factory to provide access to dynamic and permanent config and settings. | ||||
| @ -26,11 +27,10 @@ import { CONFIG_TABLE_NAME, APP_SCHEMA, ConfigDBEntry } from '@services/database | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreConfigProvider { | ||||
| 
 | ||||
|     protected appDB: Promise<SQLiteDB>; | ||||
|     protected resolveAppDB!: (appDB: SQLiteDB) => void; | ||||
|     protected dbTable: CorePromisedValue<CoreConfigTable>; | ||||
| 
 | ||||
|     constructor() { | ||||
|         this.appDB = new Promise(resolve => this.resolveAppDB = resolve); | ||||
|         this.dbTable = new CorePromisedValue(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -43,7 +43,10 @@ export class CoreConfigProvider { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
| 
 | ||||
|         this.resolveAppDB(CoreApp.getDB()); | ||||
|         const db = CoreApp.getDB(); | ||||
|         const table = await CoreConfigTable.create(db); | ||||
| 
 | ||||
|         this.dbTable.resolve(table); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -53,9 +56,9 @@ export class CoreConfigProvider { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async delete(name: string): Promise<void> { | ||||
|         const db = await this.appDB; | ||||
|         const table = await this.dbTable; | ||||
| 
 | ||||
|         await db.deleteRecords(CONFIG_TABLE_NAME, { name }); | ||||
|         await table.deleteByPrimaryKey({ name }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -66,19 +69,18 @@ export class CoreConfigProvider { | ||||
|      * @return Resolves upon success along with the config data. Reject on failure. | ||||
|      */ | ||||
|     async get<T>(name: string, defaultValue?: T): Promise<T> { | ||||
|         const db = await this.appDB; | ||||
|         const table = await this.dbTable; | ||||
|         const record = table.findByPrimaryKey({ name }); | ||||
| 
 | ||||
|         try { | ||||
|             const entry = await db.getRecord<ConfigDBEntry>(CONFIG_TABLE_NAME, { name }); | ||||
|         if (record !== null) { | ||||
|             return record.value; | ||||
|         } | ||||
| 
 | ||||
|             return entry.value; | ||||
|         } catch (error) { | ||||
|         if (defaultValue !== undefined) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|             throw error; | ||||
|         } | ||||
|         throw new Error(`Couldn't get config with name '${name}'`); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -89,11 +91,21 @@ export class CoreConfigProvider { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async set(name: string, value: number | string): Promise<void> { | ||||
|         const db = await this.appDB; | ||||
|         const table = await this.dbTable; | ||||
| 
 | ||||
|         await db.insertRecord(CONFIG_TABLE_NAME, { name, value }); | ||||
|         await table.insert({ name, value }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreConfig = makeSingleton(CoreConfigProvider); | ||||
| 
 | ||||
| /** | ||||
|  * Config database table. | ||||
|  */ | ||||
| class CoreConfigTable extends CoreDatabaseTable<ConfigDBEntry, 'name'> { | ||||
| 
 | ||||
|     protected table = CONFIG_TABLE_NAME; | ||||
|     protected primaryKeys = ['name']; | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -17,6 +17,7 @@ import { Injectable } from '@angular/core'; | ||||
| import { SQLiteDB } from '@classes/sqlitedb'; | ||||
| import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb'; | ||||
| import { makeSingleton, SQLite, Platform } from '@singletons'; | ||||
| import { CoreAppProvider } from './app'; | ||||
| 
 | ||||
| /** | ||||
|  * This service allows interacting with the local database to store and retrieve data. | ||||
| @ -24,8 +25,29 @@ import { makeSingleton, SQLite, Platform } from '@singletons'; | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreDbProvider { | ||||
| 
 | ||||
|     queryLogs: CoreDbQueryLog[] = []; | ||||
| 
 | ||||
|     protected dbInstances: {[name: string]: SQLiteDB} = {}; | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether database queries should be logged. | ||||
|      * | ||||
|      * @returns Whether queries should be logged. | ||||
|      */ | ||||
|     loggingEnabled(): boolean { | ||||
|         return CoreAppProvider.isAutomated(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Log a query. | ||||
|      * | ||||
|      * @param sql Query SQL. | ||||
|      * @param params Query parameters. | ||||
|      */ | ||||
|     logQuery(sql: string, duration: number, params?: unknown[]): void { | ||||
|         this.queryLogs.push({ sql, duration, params }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get or create a database object. | ||||
|      * | ||||
| @ -81,3 +103,12 @@ export class CoreDbProvider { | ||||
| } | ||||
| 
 | ||||
| export const CoreDB = makeSingleton(CoreDbProvider); | ||||
| 
 | ||||
| /** | ||||
|  * Database query log entry. | ||||
|  */ | ||||
| export interface CoreDbQueryLog { | ||||
|     sql: string; | ||||
|     duration: number; | ||||
|     params?: unknown[]; | ||||
| } | ||||
|  | ||||
| @ -133,12 +133,10 @@ export type WrapperComponentFixture<T> = ComponentFixture<WrapperComponent<T>>; | ||||
|  * @param overrides Object with the properties or methods to override, or a list of methods to override with an empty function. | ||||
|  * @return Mock instance. | ||||
|  */ | ||||
| export function mock<T>(instance?: T, overrides?: string[] | Record<string, unknown>): T; | ||||
| export function mock<T>(instance?: Partial<T>, overrides?: string[] | Record<string, unknown>): Partial<T>; | ||||
| export function mock<T>( | ||||
|     instance: T | Partial<T> = {}, | ||||
|     overrides: string[] | Record<string, unknown> = {}, | ||||
| ): T | Partial<T> { | ||||
| ): T { | ||||
|     // If overrides is an object, apply them to the instance.
 | ||||
|     if (!Array.isArray(overrides)) { | ||||
|         Object.assign(instance, overrides); | ||||
| @ -162,7 +160,7 @@ export function mock<T>( | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return instance; | ||||
|     return instance as T; | ||||
| } | ||||
| 
 | ||||
| export function mockSingleton<T>(singletonClass: CoreSingletonProxy<T>, instance: T): T; | ||||
|  | ||||
| @ -58,6 +58,16 @@ class performance_measure implements behat_app_listener { | ||||
|      */ | ||||
|      public $blocking; | ||||
| 
 | ||||
|     /** | ||||
|      * @var int | ||||
|      */ | ||||
|     public $databaseStart; | ||||
| 
 | ||||
|     /** | ||||
|      * @var int | ||||
|      */ | ||||
|     public $database; | ||||
| 
 | ||||
|     /** | ||||
|      * @var int | ||||
|      */ | ||||
| @ -90,6 +100,7 @@ class performance_measure implements behat_app_listener { | ||||
|         $this->start = $this->now(); | ||||
| 
 | ||||
|         $this->observeLongTasks(); | ||||
|         $this->startDatabaseCount(); | ||||
| 
 | ||||
|         $this->behatAppUnsubscribe = behat_app::listen($this); | ||||
|     } | ||||
| @ -107,6 +118,7 @@ class performance_measure implements behat_app_listener { | ||||
| 
 | ||||
|         $this->analyseDuration(); | ||||
|         $this->analyseLongTasks(); | ||||
|         $this->analyseDatabaseUsage(); | ||||
|         $this->analysePerformanceLogs(); | ||||
|     } | ||||
| 
 | ||||
| @ -131,6 +143,7 @@ class performance_measure implements behat_app_listener { | ||||
|             'styling' => $this->styling, | ||||
|             'blocking' => $this->blocking, | ||||
|             'longTasks' => count($this->longTasks), | ||||
|             'database' => $this->database, | ||||
|             'networking' => $this->networking, | ||||
|         ]; | ||||
| 
 | ||||
| @ -181,6 +194,17 @@ class performance_measure implements behat_app_listener { | ||||
|         ");
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Record how many database queries have been logged so far. | ||||
|      */ | ||||
|     private function startDatabaseCount(): void { | ||||
|         try { | ||||
|             $this->databaseStart = $this->driver->evaluateScript('dbProvider.queryLogs.length') ?? 0; | ||||
|         } catch (Exception $e) { | ||||
|             $this->databaseStart = 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Flush Performance observer. | ||||
|      */ | ||||
| @ -228,6 +252,13 @@ class performance_measure implements behat_app_listener { | ||||
|         $this->blocking = $blocking; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Analyse database usage. | ||||
|      */ | ||||
|     private function analyseDatabaseUsage(): void { | ||||
|         $this->database = $this->driver->evaluateScript('dbProvider.queryLogs.length') - $this->databaseStart; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Analyse performance logs. | ||||
|      */ | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user