Timeline performance improvements.

This commit is contained in:
Dermot Duffy
2023-01-24 19:36:54 -08:00
parent 6415597e9b
commit 5be6ee86e0
4 changed files with 112 additions and 34 deletions
+5 -3
View File
@@ -626,10 +626,12 @@ export class FrigateCardTimelineCore extends LitElement {
return null; return null;
} }
return new EventMediaQueries( const cacheFriendlyWindow = this._timelineSource.getCacheFriendlyEventWindow(
this._timelineSource.getTimelineEventQueries(
options?.window ?? this._timeline.getWindow(), options?.window ?? this._timeline.getWindow(),
), );
return new EventMediaQueries(
this._timelineSource.getTimelineEventQueries(cacheFriendlyWindow),
); );
} }
+2 -6
View File
@@ -23,7 +23,7 @@ export class MemoryRequestCache<Request, Response>
protected _data: RequestCacheItem<Request, Response>[] = []; protected _data: RequestCacheItem<Request, Response>[] = [];
public get(request: Request): Response | null { public get(request: Request): Response | null {
const now = this._now(); const now = new Date();
for (const item of this._data) { for (const item of this._data) {
if ( if (
(!item.expires || now <= item.expires) && (!item.expires || now <= item.expires) &&
@@ -50,16 +50,12 @@ export class MemoryRequestCache<Request, Response>
this._expireOldRequests(); this._expireOldRequests();
} }
protected _now(): Date {
return new Date();
}
protected _contains(a: Request, b: Request): boolean { protected _contains(a: Request, b: Request): boolean {
return isEqual(a, b); return isEqual(a, b);
} }
protected _expireOldRequests(): void { protected _expireOldRequests(): void {
const now = this._now(); const now = new Date();
this._data = this._data.filter((item) => !item.expires || now < item.expires); this._data = this._data.filter((item) => !item.expires || now < item.expires);
} }
} }
+51 -10
View File
@@ -1,4 +1,3 @@
import cloneDeep from 'lodash-es/cloneDeep';
import orderBy from 'lodash-es/orderBy'; import orderBy from 'lodash-es/orderBy';
interface Range<T extends Date | number> { interface Range<T extends Date | number> {
@@ -8,20 +7,22 @@ interface Range<T extends Date | number> {
export type DateRange = Range<Date>; export type DateRange = Range<Date>;
export class MemoryRangeSet { interface MemoryRangeSetInterface<T> {
hasCoverage(range: T): boolean;
add(range: T): void;
clear(): void;
}
export class MemoryRangeSet implements MemoryRangeSetInterface<DateRange> {
protected _ranges: DateRange[]; protected _ranges: DateRange[];
constructor(ranges?: DateRange[]) { constructor(ranges?: DateRange[]) {
this._ranges = ranges ?? []; this._ranges = ranges ?? [];
} }
public clone(): MemoryRangeSet {
return new MemoryRangeSet(cloneDeep(this._ranges));
}
public hasCoverage(range: DateRange): boolean { public hasCoverage(range: DateRange): boolean {
return this._ranges.some((cachedRange) => return this._ranges.some((cachedRange) =>
this._isEntirelyContained(cachedRange, range), rangeIsEntirelyContained(cachedRange, range),
); );
} }
@@ -30,11 +31,51 @@ export class MemoryRangeSet {
this._ranges = compressRanges(this._ranges); this._ranges = compressRanges(this._ranges);
} }
protected _isEntirelyContained(bigger: DateRange, smaller: DateRange): boolean { public clear(): void {
return smaller.start >= bigger.start && smaller.end <= bigger.end; this._ranges = [];
} }
} }
interface ExpiringRange<T extends Date | number> extends Range<T> {
expires: Date;
}
export class ExpiringMemoryRangeSet
implements MemoryRangeSetInterface<ExpiringRange<Date>>
{
protected _ranges: ExpiringRange<Date>[];
constructor(ranges?: ExpiringRange<Date>[]) {
this._ranges = ranges ?? [];
}
public hasCoverage(range: DateRange): boolean {
const now = new Date();
return this._ranges.some(
(cachedRange) =>
now < cachedRange.expires && rangeIsEntirelyContained(cachedRange, range),
);
}
public add(range: ExpiringRange<Date>): void {
this._expireOldRanges();
this._ranges.push(range);
}
protected _expireOldRanges(): void {
const now = new Date();
this._ranges = this._ranges.filter((range) => now < range.expires);
}
public clear(): void {
this._ranges = [];
}
}
const rangeIsEntirelyContained = (bigger: DateRange, smaller: DateRange): boolean => {
return smaller.start >= bigger.start && smaller.end <= bigger.end;
};
export const rangesOverlap = (a: DateRange, b: DateRange): boolean => { export const rangesOverlap = (a: DateRange, b: DateRange): boolean => {
return ( return (
// a starts within the range of b. // a starts within the range of b.
@@ -44,7 +85,7 @@ export const rangesOverlap = (a: DateRange, b: DateRange): boolean => {
// a encompasses the entire range of b. // a encompasses the entire range of b.
(a.start <= b.start && a.end >= b.end) (a.start <= b.start && a.end >= b.end)
); );
} };
export const compressRanges = <T extends Date | number>( export const compressRanges = <T extends Date | number>(
ranges: Range<T>[], ranges: Range<T>[],
+53 -14
View File
@@ -1,4 +1,5 @@
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import add from 'date-fns/add';
import sub from 'date-fns/sub'; import sub from 'date-fns/sub';
import { DataSet } from 'vis-data'; import { DataSet } from 'vis-data';
import { IdType, TimelineItem, TimelineWindow } from 'vis-timeline/esnext'; import { IdType, TimelineItem, TimelineWindow } from 'vis-timeline/esnext';
@@ -9,7 +10,11 @@ import { RecordingSegment, RecordingSegments } from './frigate';
import { capEndDate, convertRangeToCacheFriendlyTimes } from './data/data-manager-util'; import { capEndDate, convertRangeToCacheFriendlyTimes } from './data/data-manager-util';
import { EventMediaQueries } from '../view'; import { EventMediaQueries } from '../view';
import { ViewMedia } from '../view-media'; import { ViewMedia } from '../view-media';
import { compressRanges, MemoryRangeSet } from './data/data-manager-range'; import {
compressRanges,
ExpiringMemoryRangeSet,
MemoryRangeSet,
} from './data/data-manager-range';
import { ModifyInterface } from './basic'; import { ModifyInterface } from './basic';
// Allow timeline freshness to be at least this number of seconds out of date // Allow timeline freshness to be at least this number of seconds out of date
@@ -35,8 +40,15 @@ export class TimelineDataSource {
protected _dataset: DataSet<FrigateCardTimelineItem> = new DataSet(); protected _dataset: DataSet<FrigateCardTimelineItem> = new DataSet();
// The ranges in which recordings have been calculated and added for. // The ranges in which recordings have been calculated and added for.
// Calculating recordings is a very expensive process since it is based on
// segments (not just the fetch is expensive, but the JS to dedup and turn the
// high-N segments into a smaller number of consecutive recording blocks).
protected _recordingRanges = new MemoryRangeSet(); protected _recordingRanges = new MemoryRangeSet();
// Cache event ranges since re-adding the same events is a timeline
// performance killer (even if the request results are cached).
protected _eventRanges = new ExpiringMemoryRangeSet();
protected _cameraIDs: Set<string>; protected _cameraIDs: Set<string>;
protected _mediaType: ClipsOrSnapshotsOrAll; protected _mediaType: ClipsOrSnapshotsOrAll;
@@ -55,6 +67,7 @@ export class TimelineDataSource {
} }
public clearEvents(): void { public clearEvents(): void {
this._eventRanges.clear();
this._dataset.remove( this._dataset.remove(
this._dataset.get({ this._dataset.get({
filter: (item) => item.type !== 'background', filter: (item) => item.type !== 'background',
@@ -88,13 +101,16 @@ export class TimelineDataSource {
]); ]);
} }
public getTimelineEventQueries(window: TimelineWindow): EventQuery[] { public getCacheFriendlyEventWindow(window: TimelineWindow): TimelineWindow {
const _window = convertRangeToCacheFriendlyTimes(window, { return convertRangeToCacheFriendlyTimes(window, {
endCap: true, endCap: true,
}); });
}
public getTimelineEventQueries(window: TimelineWindow): EventQuery[] {
return this._dataManager.generateDefaultEventQueries(this._cameraIDs, { return this._dataManager.generateDefaultEventQueries(this._cameraIDs, {
start: _window.start, start: window.start,
end: _window.end, end: window.end,
...(this._mediaType === 'clips' && { hasClip: true }), ...(this._mediaType === 'clips' && { hasClip: true }),
...(this._mediaType === 'snapshots' && { hasSnapshot: true }), ...(this._mediaType === 'snapshots' && { hasSnapshot: true }),
}); });
@@ -105,7 +121,22 @@ export class TimelineDataSource {
cameras: Map<string, CameraConfig>, cameras: Map<string, CameraConfig>,
window: TimelineWindow, window: TimelineWindow,
): Promise<void> { ): Promise<void> {
const query = new EventMediaQueries(this.getTimelineEventQueries(window)); if (
this._eventRanges.hasCoverage({
start: window.start,
end: sub(capEndDate(window.end), {
seconds: TIMELINE_FRESHNESS_TOLERANCE_SECONDS,
}),
})
) {
return;
}
const cacheFriendlyWindow = this.getCacheFriendlyEventWindow(window);
const query = new EventMediaQueries(
this.getTimelineEventQueries(cacheFriendlyWindow),
);
const results = await this._dataManager.executeMediaQuery(hass, query); const results = await this._dataManager.executeMediaQuery(hass, query);
for (const media of results?.getResults() ?? []) { for (const media of results?.getResults() ?? []) {
const endTime = media.getEndTime(); const endTime = media.getEndTime();
@@ -123,6 +154,11 @@ export class TimelineDataSource {
}); });
} }
} }
this._eventRanges.add({
...cacheFriendlyWindow,
expires: add(new Date(), { seconds: TIMELINE_FRESHNESS_TOLERANCE_SECONDS }),
});
} }
protected async _refreshRecordings( protected async _refreshRecordings(
@@ -171,14 +207,14 @@ export class TimelineDataSource {
// Calculate an end date that's slightly short of the current time to allow // Calculate an end date that's slightly short of the current time to allow
// for caching up to the freshness tolerance. // for caching up to the freshness tolerance.
const end = sub(capEndDate(window.end), { if (
seconds: TIMELINE_FRESHNESS_TOLERANCE_SECONDS, this._recordingRanges.hasCoverage({
});
const hasCoverage = this._recordingRanges.hasCoverage({
start: window.start, start: window.start,
end: end, end: sub(capEndDate(window.end), {
}); seconds: TIMELINE_FRESHNESS_TOLERANCE_SECONDS,
if (hasCoverage) { }),
})
) {
return; return;
} }
@@ -220,6 +256,9 @@ export class TimelineDataSource {
addRecordings(compressedRecordings); addRecordings(compressedRecordings);
} }
this._recordingRanges.add({ start: window.start, end: end }); this._recordingRanges.add({
start: cacheFriendlyWindow.start,
end: cacheFriendlyWindow.end,
});
} }
} }