Timeline performance improvements.
This commit is contained in:
@@ -626,10 +626,12 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cacheFriendlyWindow = this._timelineSource.getCacheFriendlyEventWindow(
|
||||||
|
options?.window ?? this._timeline.getWindow(),
|
||||||
|
);
|
||||||
|
|
||||||
return new EventMediaQueries(
|
return new EventMediaQueries(
|
||||||
this._timelineSource.getTimelineEventQueries(
|
this._timelineSource.getTimelineEventQueries(cacheFriendlyWindow),
|
||||||
options?.window ?? this._timeline.getWindow(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>[],
|
||||||
|
|||||||
@@ -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({
|
||||||
});
|
start: window.start,
|
||||||
const hasCoverage = this._recordingRanges.hasCoverage({
|
end: sub(capEndDate(window.end), {
|
||||||
start: window.start,
|
seconds: TIMELINE_FRESHNESS_TOLERANCE_SECONDS,
|
||||||
end: end,
|
}),
|
||||||
});
|
})
|
||||||
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user