From de862fd0e76719bfa9162fefadeededd675f719c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 26 Oct 2023 11:35:55 +0200 Subject: [PATCH] Optimize video viewer stats Many Redis (and so network) calls can be expensive Avoid them if we can by using in memory cache --- server/core/lib/redis.ts | 6 +-- .../lib/views/shared/video-viewer-stats.ts | 47 +++++++++++++++++-- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/server/core/lib/redis.ts b/server/core/lib/redis.ts index 7c87bf2e3..e986f3141 100644 --- a/server/core/lib/redis.ts +++ b/server/core/lib/redis.ts @@ -352,9 +352,9 @@ class Redis { return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` } } - private generateLocalVideoViewerKeys (ip: string, videoId: number): { setKey: string, viewerKey: string } - private generateLocalVideoViewerKeys (): { setKey: string } - private generateLocalVideoViewerKeys (ip?: string, videoId?: number) { + generateLocalVideoViewerKeys (ip: string, videoId: number): { setKey: string, viewerKey: string } + generateLocalVideoViewerKeys (): { setKey: string } + generateLocalVideoViewerKeys (ip?: string, videoId?: number) { return { setKey: `local-video-viewer-stats-keys`, viewerKey: `local-video-viewer-stats-${ip}-${videoId}` } } diff --git a/server/core/lib/views/shared/video-viewer-stats.ts b/server/core/lib/views/shared/video-viewer-stats.ts index 35ef5a7ea..5b5998b0a 100644 --- a/server/core/lib/views/shared/video-viewer-stats.ts +++ b/server/core/lib/views/shared/video-viewer-stats.ts @@ -34,6 +34,8 @@ type LocalViewerStats = { export class VideoViewerStats { private processingViewersStats = false + private readonly viewerCache = new Map() + constructor () { setInterval(() => this.processViewerStats(), VIEW_LIFETIME.VIEWER_STATS) } @@ -56,7 +58,7 @@ export class VideoViewerStats { // --------------------------------------------------------------------------- async getWatchTime (videoId: number, ip: string) { - const stats: LocalViewerStats = await Redis.Instance.getLocalVideoViewer({ ip, videoId }) + const stats: LocalViewerStats = await this.getLocalVideoViewerByIP({ ip, videoId }) return stats?.watchTime || 0 } @@ -72,7 +74,7 @@ export class VideoViewerStats { const { video, ip, viewEvent, currentTime } = options const nowMs = new Date().getTime() - let stats: LocalViewerStats = await Redis.Instance.getLocalVideoViewer({ ip, videoId: video.id }) + let stats: LocalViewerStats = await this.getLocalVideoViewerByIP({ ip, videoId: video.id }) if (stats && stats.watchSections.length >= MAX_LOCAL_VIEWER_WATCH_SECTIONS) { logger.warn('Too much watch section to store for a viewer, skipping this one', { currentTime, viewEvent, ...lTags(video.uuid) }) @@ -121,7 +123,7 @@ export class VideoViewerStats { logger.debug('Set local video viewer stats for video %s.', video.uuid, { stats, ...lTags(video.uuid) }) - await Redis.Instance.setLocalVideoViewer(ip, video.id, stats) + await this.setLocalVideoViewer(ip, video.id, stats) } async processViewerStats () { @@ -136,7 +138,7 @@ export class VideoViewerStats { const allKeys = await Redis.Instance.listLocalVideoViewerKeys() for (const key of allKeys) { - const stats: LocalViewerStats = await Redis.Instance.getLocalVideoViewer({ key }) + const stats: LocalViewerStats = await this.getLocalVideoViewerByKey(key) // Process expired stats if (stats.lastUpdated > now - VIEW_LIFETIME.VIEWER_STATS) { @@ -155,7 +157,7 @@ export class VideoViewerStats { } }) - await Redis.Instance.deleteLocalVideoViewersKeys(key) + await this.deleteLocalVideoViewersKeys(key) } catch (err) { logger.error('Cannot process viewer stats for Redis key %s.', key, { err, ...lTags() }) } @@ -193,4 +195,39 @@ export class VideoViewerStats { private buildWatchTimeFromSections (sections: { start: number, end: number }[]) { return sections.reduce((p, current) => p + (current.end - current.start), 0) } + + /** + * + * Redis calls can be expensive so try to cache things in front of it + * + */ + + private getLocalVideoViewerByIP (options: { + ip: string + videoId: number + }): Promise { + const { viewerKey } = Redis.Instance.generateLocalVideoViewerKeys(options.ip, options.videoId) + + return this.getLocalVideoViewerByKey(viewerKey) + } + + private getLocalVideoViewerByKey (key: string): Promise { + const viewer = this.viewerCache.get(key) + if (viewer) return Promise.resolve(viewer) + + return Redis.Instance.getLocalVideoViewer({ key }) + } + + private setLocalVideoViewer (ip: string, videoId: number, stats: LocalViewerStats) { + const { viewerKey } = Redis.Instance.generateLocalVideoViewerKeys(ip, videoId) + this.viewerCache.set(viewerKey, stats) + + return Redis.Instance.setLocalVideoViewer(ip, videoId, stats) + } + + private deleteLocalVideoViewersKeys (key: string) { + this.viewerCache.delete(key) + + return Redis.Instance.deleteLocalVideoViewersKeys(key) + } }