From 480c3d0ab765ce94c1acf1afc9e7131fedbadacc Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 8 Mar 2025 17:31:25 -0800 Subject: [PATCH] perf: Various minor timeline performance improvements (#1952) - Closes #1930 --- src/components/timeline-core.ts | 11 +- src/view/media-queries.ts | 38 ++++++ tests/view/media-queries-results.test.ts | 2 + tests/view/media-queries.test.ts | 164 +++++++++++++++++++++++ 4 files changed, 211 insertions(+), 4 deletions(-) diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 07274dc7..b99cfe34 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -483,7 +483,6 @@ export class AdvancedCameraCardTimelineCore extends LitElement { if (panMode === 'seek') { newResults = results .clone() - .resetSelectedResult() .selectBestResult( (mediaArray) => findBestMediaIndex(mediaArray, targetTime, view?.camera), { @@ -494,7 +493,6 @@ export class AdvancedCameraCardTimelineCore extends LitElement { } else if (panMode === 'seek-in-camera') { newResults = results .clone() - .resetSelectedResult() .selectBestResult((mediaArray) => findBestMediaIndex(mediaArray, targetTime), { cameraID: view.camera, }) @@ -683,6 +681,9 @@ export class AdvancedCameraCardTimelineCore extends LitElement { return; } const mediaQuery = this._createMediaQueries(queryType); + if (!mediaQuery || this._alreadyHasAcceptableMediaQuery(mediaQuery)) { + return; + } await this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({ params: { @@ -1088,7 +1089,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement { !!this.cameraManager && !!currentQueries && !!currentResultTimestamp && - isEqual(currentQueries, freshMediaQuery.getQueries()) && + !!view?.query?.isSupersetOf(freshMediaQuery) && this.cameraManager.areMediaQueriesResultsFresh( currentQueries, currentResultTimestamp, @@ -1179,7 +1180,9 @@ export class AdvancedCameraCardTimelineCore extends LitElement { this._timelineSource && this._refTimeline.value && this.timelineConfig && - (changedProperties.has('timelineConfig') || changedProperties.has('cameraIDs')) + (!this._timeline || + changedProperties.has('timelineConfig') || + changedProperties.has('cameraIDs')) ) { if (this._timeline) { this._destroy(); diff --git a/src/view/media-queries.ts b/src/view/media-queries.ts index 2b5268c1..23c84818 100644 --- a/src/view/media-queries.ts +++ b/src/view/media-queries.ts @@ -59,6 +59,44 @@ class MediaQueriesBase { } return true; } + + public isSupersetOf(that: MediaQueries): boolean { + // Queries are typically a single item, so quadratic complexity here is + // likely still a lot better than going to the network for a new set of + // query results. + for (const thatQuery of that.getQueries() ?? []) { + let haveMatch = false; + for (const thisQuery of this._queries ?? []) { + // Compare the query except the times, and then separately compare the + // times taking into account whether source time is larger than target + // time. + if ( + isEqual( + { + ...thisQuery, + end: null, + start: null, + }, + { ...thatQuery, end: null, start: null }, + ) && + ((!thisQuery.start && !thatQuery.start) || + (thisQuery.start && + thatQuery.start && + thisQuery.start <= thatQuery.start)) && + ((!thisQuery.end && !thatQuery.end) || + (thisQuery.end && thatQuery.end && thisQuery.end >= thatQuery.end)) + ) { + haveMatch = true; + break; + } + } + + if (!haveMatch) { + return false; + } + } + return true; + } } export class EventMediaQueries extends MediaQueriesBase { diff --git a/tests/view/media-queries-results.test.ts b/tests/view/media-queries-results.test.ts index 4129af3a..d87e4266 100644 --- a/tests/view/media-queries-results.test.ts +++ b/tests/view/media-queries-results.test.ts @@ -30,9 +30,11 @@ describe('dispatchViewContextChangeEvent', () => { expect(results.selectIndex(0)).toEqual(results); expect(results.getSelectedResult()).toBeNull(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars expect(results.selectResultIfFound((_media: ViewMedia) => true)).toEqual(results); expect(results.getSelectedResult()).toBeNull(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars expect(results.selectBestResult((_media: ViewMedia[]) => null)).toEqual(results); expect(results.getSelectedResult()).toBeNull(); expect(results.getMultipleSelectedResults()).toEqual([]); diff --git a/tests/view/media-queries.test.ts b/tests/view/media-queries.test.ts index b8c9c817..1b024e42 100644 --- a/tests/view/media-queries.test.ts +++ b/tests/view/media-queries.test.ts @@ -96,6 +96,170 @@ describe('EventMediaQueries', () => { newCameraIDs, ); }); + + describe('should determine when queries are a superset', () => { + it('should return true with itself', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + expect(queries_1.isSupersetOf(queries_1)).toBeTruthy(); + }); + + it('should return true with an identical but shorter query', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + const queries_2 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T01:00:00.000Z'), + end: new Date('2025-03-07T23:00:00.000Z'), + }, + ]); + expect(queries_1.isSupersetOf(queries_2)).toBeTruthy(); + }); + + it('should return false with an identical but longer query', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + const queries_2 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-06T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + expect(queries_1.isSupersetOf(queries_2)).toBeFalsy(); + }); + + it('should return false with a non-matching query', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + const queries_2 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['DIFFERENT']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + expect(queries_1.isSupersetOf(queries_2)).toBeFalsy(); + }); + + it('should return true with a matching query where the source has multiple', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + { + type: QueryType.Event, + cameraIDs: new Set(['kitchen']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + const queries_2 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + expect(queries_1.isSupersetOf(queries_2)).toBeTruthy(); + }); + + it('should return false with a matching query where the target has multiple', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + const queries_2 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + { + type: QueryType.Event, + cameraIDs: new Set(['kitchen']), + start: new Date('2025-03-07T00:00:00.000Z'), + end: new Date('2025-03-08T00:00:00.000Z'), + }, + ]); + expect(queries_1.isSupersetOf(queries_2)).toBeFalsy(); + }); + + it('should return true when queries do not have start or end', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + }, + ]); + const queries_2 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + }, + ]); + expect(queries_1.isSupersetOf(queries_2)).toBeTruthy(); + }); + + it('should return true when target has no queries', () => { + const queries_1 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + }, + ]); + const queries_2 = new EventMediaQueries(); + expect(queries_1.isSupersetOf(queries_2)).toBeTruthy(); + }); + + it('should return false when source has no queries', () => { + const queries_1 = new EventMediaQueries(); + const queries_2 = new EventMediaQueries([ + { + type: QueryType.Event, + cameraIDs: new Set(['office']), + }, + ]); + expect(queries_1.isSupersetOf(queries_2)).toBeFalsy(); + }); + }); }); describe('RecordingMediaQueries', () => {