diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index dda847cf7a7..88c0097929c 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -16,6 +16,7 @@ import { FieldUpdateTrigger } from './common'; // frame storage by job id const frameDataCache: Record frameStep, }, + chunksUpdatedDate: { + get: () => data.chunks_updated_date, + }, }), ); @@ -592,6 +598,37 @@ function getFrameMeta(jobID, frame): SerializedFramesMetaData['frames'][0] { return frameMeta; } +async function refreshJobCacheIfOutdated(jobID: number): Promise { + const cached = frameDataCache[jobID]; + if (!cached) { + throw new Error('Frame data cache is abscent'); + } + + const META_DATA_RELOAD_PERIOD = 1 * 60 * 60 * 1000; // 1 hour + const isOutdated = (Date.now() - cached.metaFetchedTimestamp) > META_DATA_RELOAD_PERIOD; + + if (isOutdated) { + // get metadata again if outdated + const meta = await getFramesMeta('job', jobID, true); + if (new Date(meta.chunksUpdatedDate) > new Date(cached.meta.chunksUpdatedDate)) { + // chunks were re-defined. Existing data not relevant anymore + // currently we only re-write meta, remove all cached frames from provider and clear cached context images + // other parameters (e.g. chunkSize) are not supposed to be changed + cached.meta = meta; + cached.provider.cleanup(Number.MAX_SAFE_INTEGER); + for (const frame of Object.keys(cached.contextCache)) { + for (const image of Object.values(cached.contextCache[+frame].data)) { + // close images to immediate memory release + image.close(); + } + } + cached.contextCache = {}; + } + + cached.metaFetchedTimestamp = Date.now(); + } +} + export function getContextImage(jobID: number, frame: number): Promise> { return new Promise>((resolve, reject) => { if (!(jobID in frameDataCache)) { @@ -599,6 +636,7 @@ export function getContextImage(jobID: number, frame: number): Promise Promise, ): Promise { - if (!(jobID in frameDataCache)) { + const dataCacheExists = jobID in frameDataCache; + + if (!dataCacheExists) { const blockType = chunkType === 'video' ? BlockType.MP4VIDEO : BlockType.ARCHIVE; const meta = await getFramesMeta('job', jobID); @@ -718,6 +758,7 @@ export async function getFrame( frameDataCache[jobID] = { meta, + metaFetchedTimestamp: Date.now(), chunkSize, mode, startFrame, @@ -743,6 +784,22 @@ export async function getFrame( }; } + // basically the following functions may be affected if job cache is outdated + // - getFrame + // - getContextImage + // - getCachedChunks + // And from this idea we should call refreshJobCacheIfOutdated from each one + // Hovewer, following from the order, these methods are usually called + // it may lead to even more confusing behaviour + // + // Usually user first receives frame, then user receives ranges and finally user receives context images + // In this case (extremely rare, but nevertheless possible) user may get context images related to another frame + // - if cache gets outdated after getFrame() call + // - and before getContextImage() call + // - and both calls refer to the same frame that is refreshed honeypot frame and this frame has context images + // Thus, it is better to only call `refreshJobCacheIfOutdated` from getFrame() + await refreshJobCacheIfOutdated(jobID); + const frameMeta = getFrameMeta(jobID, frame); frameDataCache[jobID].provider.setRenderSize(frameMeta.width, frameMeta.height); frameDataCache[jobID].decodeForward = isPlaying; @@ -759,7 +816,7 @@ export async function getFrame( }); } -export async function getDeletedFrames(instanceType: 'job' | 'task', id): Promise> { +export async function getDeletedFrames(instanceType: 'job' | 'task', id: number): Promise> { if (instanceType === 'job') { const { meta } = frameDataCache[id]; return meta.deletedFrames; diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 5ec9e21d0b7..945f98f90d2 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -459,6 +459,7 @@ export interface SerializedFramesMetaData { deleted_frames: number[]; included_frames: number[]; frame_filter: string; + chunks_updated_date: string; frames: { width: number; height: number;