diff --git a/Src/Myc.Core.DataCache.pas b/Src/Myc.Core.DataCache.pas new file mode 100644 index 0000000..0c2b5e4 --- /dev/null +++ b/Src/Myc.Core.DataCache.pas @@ -0,0 +1,172 @@ +unit Myc.Core.DataCache; + +interface + +uses + System.SysUtils, + System.Generics.Collections, + System.Generics.Defaults, + Myc.Futures, // Required for TFuture + Myc.Signals; // Required for TLatch + +type + // Represents a single cached file entry with its metadata and loaded data future. + // TArrayItemType is the type of a single item within the TArray that is loaded by the future. + TCachedFile = class + Name: String; // The full path and name of the cached file. + Age: TDateTime; // The modification timestamp of the file at the time of caching. + LastUsed: TDateTime; // The last time this cached entry was accessed. + Data: TFuture>; // A future representing the asynchronously loaded data. + end; + + // Generic in-memory cache for data files, supporting asynchronous loading + // and eviction based on memory pressure and last usage. + // TArrayItemType is the type of a single item within the TArray that is loaded by the future, + // e.g., TDataPoint. + TDataCache = class + strict private + FCachedFiles: TDictionary>; + FLoadGate: TLatch; + FIsMemoryLow: TFunc; + + private + // Manages cache size by evicting least recently used items if memory is low. + procedure ManageCacheSize(const CurrentFileNameBeingLoaded: string); + + public + constructor Create(const ALoadGate: TLatch; const AIsMemoryLow: TFunc); + destructor Destroy; override; + + // Clears all entries from the cache. + procedure Clear; + + // Attempts to retrieve data for a given file from the cache. + // If found and not stale, updates LastUsed and returns true. + // Otherwise, returns false. + function TryGetValue(const FileName: string; out ACachedFile: TCachedFile): Boolean; + + // Adds a new or updates an existing entry in the cache. + procedure AddOrUpdate(const FileName: string; const AAge: TDateTime; const AData: TFuture>); + + // Returns the TLoadGate associated with this cache, for coordination of load operations. + function GetLoadGate: TLatch; inline; + + property LoadGate: TLatch read GetLoadGate; + end; + +implementation + +uses + System.Math; + +{ TDataCache } + +constructor TDataCache.Create(const ALoadGate: TLatch; const AIsMemoryLow: TFunc); +begin + inherited Create; + FCachedFiles := TObjectDictionary>.Create([doOwnsValues]); + FLoadGate := ALoadGate; + FIsMemoryLow := AIsMemoryLow; +end; + +destructor TDataCache.Destroy; +begin + FCachedFiles.Free; + inherited; +end; + +procedure TDataCache.Clear; +begin + FCachedFiles.Clear; +end; + +procedure TDataCache.AddOrUpdate( + const FileName: string; + const AAge: TDateTime; + const AData: TFuture> +); +var + cachedFile: TCachedFile; +begin + if FCachedFiles.TryGetValue(FileName, cachedFile) then + begin + // Update existing entry + cachedFile.Age := AAge; + cachedFile.Data := AData; // This will replace the old future, potentially freeing old data + cachedFile.LastUsed := Now; + end + else + begin + // Add new entry + cachedFile := TCachedFile.Create; + cachedFile.Name := FileName; + cachedFile.Age := AAge; + cachedFile.Data := AData; + cachedFile.LastUsed := Now; + FCachedFiles.Add(FileName, cachedFile); + end; +end; + +function TDataCache.GetLoadGate: TLatch; +begin + Result := FLoadGate; +end; + +procedure TDataCache.ManageCacheSize(const CurrentFileNameBeingLoaded: string); +begin + if Assigned(FIsMemoryLow) and FIsMemoryLow() then + begin + var fL := FCachedFiles.Values.ToArray; + // Sorts the cached files by LastUsed in ascending order (least recently used first). + TArray.Sort>( + fL, + TComparer>.Construct( + function(const Left, Right: TCachedFile): Integer begin Result := Sign(Left.LastUsed - Right.LastUsed) end + ) + ); + + for var i := 0 to High(fL) do + begin + if not FIsMemoryLow() then // Stop evicting if memory pressure is relieved + break; + // Do not remove the file that is currently being loaded. + if fL[i].Name <> CurrentFileNameBeingLoaded then + FCachedFiles.Remove(fL[i].Name); + end; + end; +end; + +function TDataCache.TryGetValue(const FileName: string; out ACachedFile: TCachedFile): Boolean; +var + age: TDateTime; +begin + ManageCacheSize( FileName ); + + Result := FCachedFiles.TryGetValue(FileName, ACachedFile); + if Result then + begin + // Check if the file on disk has changed since it was cached. + if FileAge(FileName, age, true) then + begin + if age = ACachedFile.Age then + begin + // Cache hit and not stale, update LastUsed. + ACachedFile.LastUsed := Now; + end + else + begin + // File is stale, remove from cache. + FCachedFiles.Remove(FileName); + Result := False; + end; + end + else + begin + // File no longer exists, remove from cache. + FCachedFiles.Remove(FileName); + Result := False; + end; + end; +end; + +end.