Files
MycLib/Src/Myc.Core.FileCache.pas
T
Michael Schimmel a9aff8c41a AuraTrader Sample
2025-06-17 21:49:09 +02:00

192 lines
5.7 KiB
ObjectPascal

unit Myc.Core.FileCache;
interface
uses
System.SysUtils,
System.IOUtils,
System.Generics.Collections,
System.Generics.Defaults,
System.SyncObjs,
Myc.Futures;
type
// Manages a cache for time-series data files loaded asynchronously.
// The cache is activated by assigning a function to the IsMemoryLow property.
// It evicts least-recently-used items when memory is low.
TDataFileCache<T> = class
type
TLoaderFunc = reference to function(const Filename: String): TFuture<T>;
private
type
// Holds metadata for a cached file.
TCachedFile = class
public
Name: String;
Age: TDateTime;
LastUsed: TDateTime;
Data: TFuture<T>;
end;
private
FCachedFiles: TDictionary<String, TCachedFile>;
FLoader: TLoaderFunc;
FLock: TCriticalSection;
public
constructor Create(const ALoader: TLoaderFunc);
destructor Destroy; override;
// Removes all items from the cache, waiting for any pending operations to complete.
procedure Clear;
// Retrieves a data future from the cache or uses the provided loader function to create and cache it.
// Caching is only active if the IsMemoryLow property is assigned.
function GetOrAdd(const AFileName: string): TFuture<T>;
end;
var
IsMemoryLow: TFunc<Boolean>;
implementation
uses
{$IFDEF MSWINDOWS}
Winapi.Windows,
{$endif}
System.Math;
{ TDataFileCache<T> }
constructor TDataFileCache<T>.Create(const ALoader: TLoaderFunc);
begin
inherited Create;
FLoader := ALoader;
FLock := TCriticalSection.Create;
FCachedFiles := TObjectDictionary<String, TCachedFile>.Create([doOwnsValues]);
end;
destructor TDataFileCache<T>.Destroy;
begin
Clear;
FCachedFiles.Free;
FLock.Free;
inherited;
end;
procedure TDataFileCache<T>.Clear;
begin
FLock.Enter;
try
// Wait for any pending load operations to finish before clearing.
for var cachedFile in FCachedFiles.Values do
cachedFile.Data.WaitFor;
FCachedFiles.Clear;
finally
FLock.Leave;
end;
end;
function TDataFileCache<T>.GetOrAdd(const AFileName: string): TFuture<T>;
var
fileList: TArray<TCachedFile>;
i: Integer;
cachedFile: TCachedFile;
currentFileAge: TDateTime;
begin
// If no memory check function is assigned, caching is disabled.
// Simply execute the loader function and return the result.
if not Assigned(IsMemoryLow) then
begin
Result := FLoader(AFilename);
exit;
end;
FLock.Enter;
try
if IsMemoryLow() then
begin
// Caching is active. First, try to free memory if needed.
fileList := FCachedFiles.Values.ToArray;
TArray.Sort<TCachedFile>(
fileList,
TComparer<TCachedFile>.Construct(
function(const Left, Right: TCachedFile): Integer
begin
// Sort by LastUsed timestamp, oldest first.
Result := Sign(Left.LastUsed - Right.LastUsed);
end
)
);
for i := 0 to High(fileList) do
begin
if not IsMemoryLow() then
break; // Stop evicting if memory pressure is relieved.
if fileList[i].Name <> AFileName then
begin
if fileList[i].Data.Done.IsSet then
begin
FCachedFiles.Remove(fileList[i].Name);
fileList[i].Data := TFuture<T>.Null;
end;
end;
end;
end;
// Check if a valid entry exists in the cache.
if FCachedFiles.TryGetValue(AFileName, cachedFile) then
begin
if FileAge(AFileName, currentFileAge, true) and (currentFileAge = cachedFile.Age) then
begin
// Cache hit and file is unchanged. Update usage timestamp and return cached future.
cachedFile.LastUsed := Now;
exit(cachedFile.Data);
end
else
begin
// File is stale or no longer exists. Remove from cache.
FCachedFiles.Remove(AFileName);
end;
end;
// Cache miss or stale entry. Execute the loader function.
Result := FLoader(AFilename);
// Create a new cache entry for the loaded future.
cachedFile := TCachedFile.Create;
cachedFile.Name := AFileName;
if FileAge(AFileName, cachedFile.Age, true) then
begin
cachedFile.Data := Result;
cachedFile.LastUsed := Now;
FCachedFiles.Add(AFileName, cachedFile);
end
else
begin
// If we cannot get the file's age, we cannot cache it reliably.
cachedFile.Free;
end;
finally
FLock.Leave;
end;
end;
initialization
{$IFDEF MSWINDOWS}
IsMemoryLow :=
function: Boolean
var
memStatusEx: TMemoryStatusEx;
begin
Result := false;
memStatusEx.dwLength := SizeOf(TMemoryStatusEx); // Initialize the structure size
if GlobalMemoryStatusEx(memStatusEx) then
begin
// memStatusEx.dwMemoryLoad indicates the approximate percentage of physical memory that is in use.
Result := memStatusEx.dwMemoryLoad >= 80;
end;
end;
{$endif}
end.