Files
MycLib/Src/Myc.Core.DataCache.pas
T
Michael Schimmel 590e98d614 DataCache
2025-06-07 14:51:39 +02:00

173 lines
5.8 KiB
ObjectPascal

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<TArrayItemType> = 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<TArray<TArrayItemType>>; // 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<TAskBidItem>.
TDataCache<TArrayItemType> = class
strict private
FCachedFiles: TDictionary<String, TCachedFile<TArrayItemType>>;
FLoadGate: TLatch;
FIsMemoryLow: TFunc<Boolean>;
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<Boolean>);
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<TArrayItemType>): Boolean;
// Adds a new or updates an existing entry in the cache.
procedure AddOrUpdate(const FileName: string; const AAge: TDateTime; const AData: TFuture<TArray<TArrayItemType>>);
// 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<TArrayItemType> }
constructor TDataCache<TArrayItemType>.Create(const ALoadGate: TLatch; const AIsMemoryLow: TFunc<Boolean>);
begin
inherited Create;
FCachedFiles := TObjectDictionary<String, TCachedFile<TArrayItemType>>.Create([doOwnsValues]);
FLoadGate := ALoadGate;
FIsMemoryLow := AIsMemoryLow;
end;
destructor TDataCache<TArrayItemType>.Destroy;
begin
FCachedFiles.Free;
inherited;
end;
procedure TDataCache<TArrayItemType>.Clear;
begin
FCachedFiles.Clear;
end;
procedure TDataCache<TArrayItemType>.AddOrUpdate(
const FileName: string;
const AAge: TDateTime;
const AData: TFuture<TArray<TArrayItemType>>
);
var
cachedFile: TCachedFile<TArrayItemType>;
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<TArrayItemType>.Create;
cachedFile.Name := FileName;
cachedFile.Age := AAge;
cachedFile.Data := AData;
cachedFile.LastUsed := Now;
FCachedFiles.Add(FileName, cachedFile);
end;
end;
function TDataCache<TArrayItemType>.GetLoadGate: TLatch;
begin
Result := FLoadGate;
end;
procedure TDataCache<TArrayItemType>.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<TCachedFile<TArrayItemType>>(
fL,
TComparer<TCachedFile<TArrayItemType>>.Construct(
function(const Left, Right: TCachedFile<TArrayItemType>): 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<TArrayItemType>.TryGetValue(const FileName: string; out ACachedFile: TCachedFile<TArrayItemType>): 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.