DataFeed Refactoring

This commit is contained in:
Michael Schimmel
2025-12-10 22:30:53 +01:00
parent a3f6f4af26
commit 3ed5a4011f
4 changed files with 227 additions and 205 deletions
+36 -28
View File
@@ -115,8 +115,8 @@ type
FProcessDone: TState; FProcessDone: TState;
FApplication: IAuraApplication; FApplication: IAuraApplication;
FModulesItem: TTreeViewItem; FModulesItem: TTreeViewItem;
FFileCache: IDataFileCache<TArray<TScalarRecordFeed.TBatch>>; FFileCache: IDataFileCache<TArray<TScalarBatch>>;
FFeed: IScalarRecordFeed; FFeed: IScalarBatchLoader;
function SelectedSymbol: String; function SelectedSymbol: String;
procedure ExecuteStrategy(const Symbol: String; Timeframe: TTimeframe; const Consumer: IConsumer<TDataPoint<TOhlcItem>>); procedure ExecuteStrategy(const Symbol: String; Timeframe: TTimeframe; const Consumer: IConsumer<TDataPoint<TOhlcItem>>);
@@ -197,7 +197,7 @@ procedure TForm1.FormCreate(Sender: TObject);
begin begin
FApplication := TMycAuraApplication.Create; FApplication := TMycAuraApplication.Create;
FFileCache := TDataFileCache<TArray<TScalarRecordFeed.TBatch>>.Create; FFileCache := TDataFileCache<TArray<TScalarBatch>>.Create;
FModulesItem := TTreeViewItem.Create(Self); FModulesItem := TTreeViewItem.Create(Self);
FModulesItem.Text := 'Modules'; FModulesItem.Text := 'Modules';
@@ -386,11 +386,40 @@ begin
var terminated := TFlag.CreateObserver(FTerminate.Signal).State; var terminated := TFlag.CreateObserver(FTerminate.Signal).State;
var path := '\\COFFEE\TickData\Pepperstone'; // Or FServer.Path if compatible var path := '\\COFFEE\TickData\Pepperstone'; // Or FServer.Path if compatible
FFeed := TScalarRecordFeed.Create(FFileCache, path, '.m1', true, def); FFeed :=
TScalarBatchLoader.Create(
FFileCache,
path,
'.m1',
true,
SizeOf(TM1FileItem),
procedure(const Src: Pointer; Dst: TScalar.PValue)
var
Item: ^TM1FileItem;
begin
Item := Src;
// Field 0: Time (DateTime is stored as Double/OADate in TScalar)
Dst.AsDouble := Item.OADateTime;
// Move to next field in destination (TScalar values are contiguous)
Inc(Dst);
// Field 1: Close (Float)
// Convert Int64 price to Double using Digits: Price / 10^Digits
if Item.Digits > 0 then
Dst.AsDouble := Item.Close / Power(10, Item.Digits)
else
Dst.AsDouble := Item.Close;
end
);
// Converter: Transforms raw IRecordSeries (Columns) into TArray<TDataPoint> // Converter: Transforms raw IRecordSeries (Columns) into TArray<TDataPoint>
var unpacker: TConverter<TScalarBatch, IScalarRecord> := TScalarBatchUnpacker.Create(def);
var ticker := var ticker :=
FFeed.Ticker.Chain<TDataPoint<Double>>( unpacker.Producer.Chain<TDataPoint<Double>>(
function(const Tick: IScalarRecord): TDataPoint<Double> function(const Tick: IScalarRecord): TDataPoint<Double>
begin begin
Result.Time := Tick[iTime].Value.Value.AsDouble; Result.Time := Tick[iTime].Value.Value.AsDouble;
@@ -398,6 +427,8 @@ begin
end end
); );
FFeed.BatchProducer.Chain(unpacker.Consumer);
// Link Chart to Ticker // Link Chart to Ticker
chart chart
.SetXAxisSeries(TTimeframe.M, ticker.Chain<TDateTime>(function(const p: TDataPoint<Double>): TDateTime begin Result := p.Time end)); .SetXAxisSeries(TTimeframe.M, ticker.Chain<TDateTime>(function(const p: TDataPoint<Double>): TDateTime begin Result := p.Time end));
@@ -408,29 +439,6 @@ begin
TAlphaColors.Orange TAlphaColors.Orange
); );
FFeed.SetImportOptions(
SizeOf(TM1FileItem),
procedure(const Src: Pointer; Dst: TScalar.PValue)
var
Item: ^TM1FileItem;
begin
Item := Src;
// Field 0: Time (DateTime is stored as Double/OADate in TScalar)
Dst.AsDouble := Item.OADateTime;
// Move to next field in destination (TScalar values are contiguous)
Inc(Dst);
// Field 1: Close (Float)
// Convert Int64 price to Double using Digits: Price / 10^Digits
if Item.Digits > 0 then
Dst.AsDouble := Item.Close / Power(10, Item.Digits)
else
Dst.AsDouble := Item.Close;
end
);
// Connect pipeline: Server -> Converter // Connect pipeline: Server -> Converter
FProcessDone := FFeed.ProcessData(symbol, terminated); FProcessDone := FFeed.ProcessData(symbol, terminated);
end; end;
-18
View File
@@ -130,16 +130,6 @@ type
class operator Trunc(const A: TScalar): TScalar; inline; class operator Trunc(const A: TScalar): TScalar; inline;
end; end;
// An array of scalar values of the same kind.
TScalarArray = record
public
Kind: TScalar.TKind;
Items: TArray<TScalar.TValue>;
constructor Create(AKind: TScalar.TKind; const AItems: TArray<TScalar.TValue>);
end;
TScalarTuple = TArray<TScalar>;
// A field definition for a scalar record. // A field definition for a scalar record.
TScalarRecordField = TPair<IKeyword, TScalar.TKind>; TScalarRecordField = TPair<IKeyword, TScalar.TKind>;
IScalarRecordDefinition = IKeywordMapping<TScalar.TKind>; IScalarRecordDefinition = IKeywordMapping<TScalar.TKind>;
@@ -792,14 +782,6 @@ begin
Result := FromInt64(System.Trunc(val)); Result := FromInt64(System.Trunc(val));
end; end;
{ TScalarArray }
constructor TScalarArray.Create(AKind: TScalar.TKind; const AItems: TArray<TScalar.TValue>);
begin
Kind := AKind;
Items := AItems;
end;
{ TScalarRecordSeries } { TScalarRecordSeries }
constructor TScalarRecordSeries.Create(const ADef: IScalarRecordDefinition); constructor TScalarRecordSeries.Create(const ADef: IScalarRecordDefinition);
+191 -137
View File
@@ -8,6 +8,7 @@ uses
System.Generics.Collections, System.Generics.Collections,
System.Generics.Defaults, System.Generics.Defaults,
System.IOUtils, System.IOUtils,
System.TimeSpan,
Myc.Futures, Myc.Futures,
Myc.Signals, Myc.Signals,
Myc.Mutable, Myc.Mutable,
@@ -18,30 +19,43 @@ uses
Myc.Core.FileCache; Myc.Core.FileCache;
type type
// Callback to convert raw bytes from a file into a TScalar.TValue sequence
TImportConverter = reference to procedure(const Src: Pointer; Dst: TScalar.PValue); TImportConverter = reference to procedure(const Src: Pointer; Dst: TScalar.PValue);
IScalarRecordFeed = interface // Represents a raw block of data read from disk
{$region 'private'} TScalarBatch = record
function GetTicker: TProducer<IScalarRecord>; Data: TArray<TScalar.TValue>;
{$endregion} RecCount: Integer;
procedure SetImportOptions(RecordSize: Integer; const Converter: TImportConverter);
function EnumerateSymbols: TFuture<TArray<String>>;
// Streaming processing: Reads data from disk and broadcasts it via Ticker
function ProcessData(const Symbol: String; const Terminated: TState): TState;
property Ticker: TProducer<IScalarRecord> read GetTicker;
end; end;
TScalarRecordFeed = class(TInterfacedObject, IScalarRecordFeed) // Converts a TScalarBatch into a stream of IScalarRecord events using the cursor pattern.
// NOTE: This uses a zero-copy approach. Consumers must process data synchronously or copy it.
TScalarBatchUnpacker = class(TMycConverter<TScalarBatch, IScalarRecord>)
private
FDef: IScalarRecordDefinition;
protected
function Consume(const Value: TScalarBatch): TState; override;
public
constructor Create(const ADef: IScalarRecordDefinition);
end;
IScalarBatchLoader = interface
{$region 'private'}
function GetBatchProducer: TProducer<TScalarBatch>;
{$endregion}
function EnumerateSymbols: TFuture<TArray<String>>;
// Streaming processing: Reads data from disk and broadcasts it via BatchProducer
function ProcessData(const Symbol: String; const Terminated: TState): TState;
// Provides raw TScalarBatch events
property BatchProducer: TProducer<TScalarBatch> read GetBatchProducer;
end;
TScalarBatchLoader = class(TInterfacedObject, IScalarBatchLoader)
public public
type type
TBatch = record
Data: TArray<TScalar.TValue>;
RecCount: Integer;
end;
TDataFile = record TDataFile = record
private private
FExtension: String; FExtension: String;
@@ -65,16 +79,15 @@ type
var var
FPath: String; FPath: String;
FDef: IScalarRecordDefinition;
FLookback: Int64; FLookback: Int64;
FFileExtension: String; FFileExtension: String;
FIsCompressed: Boolean; FIsCompressed: Boolean;
FCachedFiles: IDataFileCache<TArray<TBatch>>; FCachedFiles: IDataFileCache<TArray<TScalarBatch>>;
FSymbols: TFuture<TDictionary<String, TArray<TDataFile>>>; FSymbols: TFuture<TDictionary<String, TArray<TDataFile>>>;
// Producer implementation // Internal producer for raw batches
FTicker: TMycContainedProducer<IScalarRecord>; FBatchProducer: TMycContainedProducer<TScalarBatch>;
class var class var
FLoadGate: TLatch; FLoadGate: TLatch;
@@ -82,54 +95,57 @@ type
function GetPath: String; function GetPath: String;
// Loading Logic // Loading Logic
function DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TBatch>>; function DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TScalarBatch>>;
function LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TBatch>>; function LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TScalarBatch>>;
// Processing Logic // Processing Logic
function ProcessFile( function ProcessFile(
const Files: TArray<TDataFile>; const Files: TArray<TDataFile>;
Index: Integer; Index: Integer;
const DataFile: TFuture<TArray<TBatch>>; const DataFile: TFuture<TArray<TScalarBatch>>;
const Terminated: TState const Terminated: TState
): TState; ): TState;
function ProcessChunks(const Batches: TArray<TBatch>; Terminated: TState): TState; function ProcessChunks(const Batches: TArray<TScalarBatch>; Terminated: TState): TState;
// Binary I/O // Binary I/O
function ReadCompressedData(const InputStream: TStream): TArray<TBatch>; function ReadCompressedData(const InputStream: TStream): TArray<TScalarBatch>;
function ReadUncompressedData(const InputStream: TStream): TArray<TBatch>; function ReadUncompressedData(const InputStream: TStream): TArray<TScalarBatch>;
// Helpers // Helpers
function ParseFileName(const FileName: string): TDataFile; function ParseFileName(const FileName: string): TDataFile;
procedure UpdateSymbols; procedure UpdateSymbols;
private private
FImportRecordSize: Integer; FRecordSize: Integer;
FImportConverter: TImportConverter; FConverter: TImportConverter;
function GetTicker: TProducer<IScalarRecord>; function GetBatchProducer: TProducer<TScalarBatch>;
public public
constructor Create( constructor Create(
const ACachedFiles: IDataFileCache<TArray<TBatch>>; const ACachedFiles: IDataFileCache<TArray<TScalarBatch>>;
const APath, AExtension: String; const APath, AExtension: String;
IsCompressed: Boolean; IsCompressed: Boolean;
const ADef: IScalarRecordDefinition // RecordSize defines both the input bytes per record AND the output TValues per record
ARecordSize: Integer;
const AConverter: TImportConverter
); );
destructor Destroy; override; destructor Destroy; override;
procedure AfterConstruction; override; procedure AfterConstruction; override;
procedure SetImportOptions(RecordSize: Integer; const Converter: TImportConverter);
function EnumerateSymbols: TFuture<TArray<String>>; function EnumerateSymbols: TFuture<TArray<String>>;
// Streaming processing: Reads data from disk and broadcasts it via Ticker // Streaming processing: Reads data from disk and broadcasts it via BatchTicker
function ProcessData(const Symbol: String; const Terminated: TState): TState; function ProcessData(const Symbol: String; const Terminated: TState): TState;
function CreateTicker(const Def: IScalarRecordDefinition): TConverter<TScalarBatch, IScalarRecord>;
property Path: String read GetPath; property Path: String read GetPath;
property FileExtension: String read FFileExtension; property FileExtension: String read FFileExtension;
property IsCompressed: Boolean read FIsCompressed; property IsCompressed: Boolean read FIsCompressed;
property Lookback: Int64 read FLookback write FLookback; property Lookback: Int64 read FLookback write FLookback;
property Ticker: TProducer<IScalarRecord> read GetTicker; property BatchProducer: TProducer<TScalarBatch> read GetBatchProducer;
end; end;
implementation implementation
@@ -144,8 +160,8 @@ const
BatchSize = 1024; BatchSize = 1024;
type type
// Private implementation of the view logic. // Private implementation of the view logic (Cursor Pattern).
// This object is reused (cursor pattern) to avoid allocations per tick. // This object is reused to avoid allocations per tick.
TScalarRecordView = class(TInterfacedObject, IScalarRecord) TScalarRecordView = class(TInterfacedObject, IScalarRecord)
private private
FDef: IScalarRecordDefinition; FDef: IScalarRecordDefinition;
@@ -204,9 +220,42 @@ begin
Result := FDef.IndexOf(Key); Result := FDef.IndexOf(Key);
end; end;
{ TScalarRecordFeed.TDataFile } { TScalarBatchUnpacker }
constructor TScalarRecordFeed.TDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer; const ABasename: String); constructor TScalarBatchUnpacker.Create(const ADef: IScalarRecordDefinition);
begin
inherited Create;
FDef := ADef;
end;
function TScalarBatchUnpacker.Consume(const Value: TScalarBatch): TState;
begin
if (Length(Value.Data) > 0) and (Value.RecCount > 0) then
begin
var Stride := Length(Value.Data) div Value.RecCount;
var pCursor: TScalar.PValue := @Value.Data[0];
// Initialize reused View object (Cursor Pattern)
// This object implements IScalarRecord and is updated for each tick.
var ViewObj := TScalarRecordView.Create(FDef);
var View := ViewObj as IScalarRecord;
for var i := 0 to Value.RecCount - 1 do
begin
ViewObj.SetData(pCursor);
// Broadcast to all listeners
Broadcast(View);
Inc(pCursor, Stride);
end;
end;
Result := TState.Null;
end;
{ TScalarBatchLoader.TDataFile }
constructor TScalarBatchLoader.TDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer; const ABasename: String);
begin begin
FPath := APath; FPath := APath;
FSymbol := ASymbol; FSymbol := ASymbol;
@@ -216,70 +265,111 @@ begin
FBasename := ABasename; FBasename := ABasename;
end; end;
function TScalarRecordFeed.TDataFile.GetFullFileName: string; function TScalarBatchLoader.TDataFile.GetFullFileName: string;
begin begin
Result := TPath.Combine(FPath, FBasename + FExtension); Result := TPath.Combine(FPath, FBasename + FExtension);
end; end;
function TScalarRecordFeed.TDataFile.GetIsValid: Boolean; function TScalarBatchLoader.TDataFile.GetIsValid: Boolean;
begin begin
Result := (FSymbol <> '') and (FYear > 0); Result := (FSymbol <> '') and (FYear > 0);
end; end;
{ TScalarRecordFeed } { TScalarBatchLoader }
constructor TScalarRecordFeed.Create( constructor TScalarBatchLoader.Create(
const ACachedFiles: IDataFileCache<TArray<TBatch>>; const ACachedFiles: IDataFileCache<TArray<TScalarBatch>>;
const APath, AExtension: String; const APath, AExtension: String;
IsCompressed: Boolean; IsCompressed: Boolean;
const ADef: IScalarRecordDefinition ARecordSize: Integer;
const AConverter: TImportConverter
); );
begin begin
inherited Create; inherited Create;
FPath := APath; FPath := APath;
FDef := ADef;
FCachedFiles := ACachedFiles; FCachedFiles := ACachedFiles;
FFileExtension := AExtension; FFileExtension := AExtension;
FIsCompressed := IsCompressed; FIsCompressed := IsCompressed;
FLookback := -1; FLookback := -1;
FImportRecordSize := 0;
FImportConverter := nil;
FTicker := TMycContainedProducer<IScalarRecord>.Create(Self); // RecordSize is used for both Input Bytes Stride AND Output TValues Count
FRecordSize := ARecordSize;
FConverter := AConverter;
FCachedFiles := TDataFileCache<TArray<TBatch>>.Create; Assert(FRecordSize > 0);
Assert(Assigned(FConverter));
FBatchProducer := TMycContainedProducer<TScalarBatch>.Create(Self);
FCachedFiles := TDataFileCache<TArray<TScalarBatch>>.Create;
end; end;
destructor TScalarRecordFeed.Destroy; destructor TScalarBatchLoader.Destroy;
begin begin
FCachedFiles.Clear; FCachedFiles.Clear;
FTicker.Free; FBatchProducer.Free;
inherited Destroy; inherited Destroy;
end; end;
procedure TScalarRecordFeed.AfterConstruction; procedure TScalarBatchLoader.AfterConstruction;
begin begin
inherited; inherited;
UpdateSymbols; UpdateSymbols;
end; end;
procedure TScalarRecordFeed.SetImportOptions(RecordSize: Integer; const Converter: TImportConverter); function TScalarBatchLoader.CreateTicker(const Def: IScalarRecordDefinition): TConverter<TScalarBatch, IScalarRecord>;
var
viewObj: TScalarRecordView;
view: IScalarRecord;
begin begin
FImportRecordSize := RecordSize; Result :=
FImportConverter := Converter; TConverter<TScalarBatch, IScalarRecord>.CreateAggregation(
function(const Value: TScalarBatch; const Broadcast: TBroadcastFunc<IScalarRecord>): TState
begin
if (Length(Value.Data) > 0) and (Value.RecCount > 0) then
begin
var Stride := Length(Value.Data) div Value.RecCount;
var pCursor: TScalar.PValue := @Value.Data[0];
for var i := 0 to Value.RecCount - 1 do
begin
if view = nil then
begin
// Initialize reused View object (Cursor Pattern)
// This object implements IScalarRecord and is updated for each tick.
viewObj := TScalarRecordView.Create(Def);
view := viewObj as IScalarRecord;
end;
var rc := viewObj._AddRef;
try
// Broadcast tick
viewObj.SetData(pCursor);
Broadcast(view);
finally
// If client picked it up, we release it
if viewObj._Release >= rc then
view := nil;
end;
Inc(pCursor, Stride);
end;
end;
Result := TState.Null;
end
);
end; end;
function TScalarRecordFeed.GetPath: String; function TScalarBatchLoader.GetPath: String;
begin begin
Result := FPath; Result := FPath;
end; end;
function TScalarRecordFeed.GetTicker: TProducer<IScalarRecord>; function TScalarBatchLoader.GetBatchProducer: TProducer<TScalarBatch>;
begin begin
Result := FTicker; Result := FBatchProducer;
end; end;
procedure TScalarRecordFeed.UpdateSymbols; procedure TScalarBatchLoader.UpdateSymbols;
begin begin
FSymbols := FSymbols :=
TFuture<TDictionary<String, TArray<TDataFile>>>.Construct( TFuture<TDictionary<String, TArray<TDataFile>>>.Construct(
@@ -340,7 +430,7 @@ begin
FSymbols.Manage; FSymbols.Manage;
end; end;
function TScalarRecordFeed.EnumerateSymbols: TFuture<TArray<String>>; function TScalarBatchLoader.EnumerateSymbols: TFuture<TArray<String>>;
begin begin
Result := Result :=
FSymbols.Chain<TArray<String>>( FSymbols.Chain<TArray<String>>(
@@ -348,7 +438,7 @@ begin
); );
end; end;
function TScalarRecordFeed.ParseFileName(const FileName: string): TDataFile; function TScalarBatchLoader.ParseFileName(const FileName: string): TDataFile;
var var
path, fileNameNoPath, baseName, ext, symbol: string; path, fileNameNoPath, baseName, ext, symbol: string;
year, month: Integer; year, month: Integer;
@@ -389,21 +479,21 @@ begin
Result := TDataFile.Create(path, symbol, ext, year, month, baseName); Result := TDataFile.Create(path, symbol, ext, year, month, baseName);
end; end;
function TScalarRecordFeed.LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TBatch>>; function TScalarBatchLoader.LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TScalarBatch>>;
begin begin
if DataFile.IsValid then if DataFile.IsValid then
Result := Result :=
FCachedFiles.GetOrAdd( FCachedFiles.GetOrAdd(
DataFile.GetFullFileName, DataFile.GetFullFileName,
function(const Filename: String): TFuture<TArray<TBatch>> begin Result := DoLoad(Filename, FLoadGate); end function(const Filename: String): TFuture<TArray<TScalarBatch>> begin Result := DoLoad(Filename, FLoadGate); end
) )
else else
Result := TFuture<TArray<TBatch>>.Construct(TArray<TBatch>(nil)); Result := TFuture<TArray<TScalarBatch>>.Construct(TArray<TScalarBatch>(nil));
end; end;
function TScalarRecordFeed.DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TBatch>>; function TScalarBatchLoader.DoLoad(const FileName: string; const Gate: TLatch): TFuture<TArray<TScalarBatch>>;
begin begin
Result := TFuture<TArray<TBatch>>.Null; Result := TFuture<TArray<TScalarBatch>>.Null;
if not TFile.Exists(FileName) then if not TFile.Exists(FileName) then
exit; exit;
@@ -414,8 +504,8 @@ begin
Result := Result :=
TFuture<TBytes> TFuture<TBytes>
.Construct(Gate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end) .Construct(Gate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
.Chain<TArray<TBatch>>( .Chain<TArray<TScalarBatch>>(
function(bytes: TBytes): TArray<TBatch> function(bytes: TBytes): TArray<TScalarBatch>
begin begin
var ms := TBytesStream.Create(bytes); var ms := TBytesStream.Create(bytes);
try try
@@ -428,9 +518,9 @@ begin
else else
begin begin
Result := Result :=
TFuture<TArray<TBatch>>.Construct( TFuture<TArray<TScalarBatch>>.Construct(
Gate.Enqueue, Gate.Enqueue,
function: TArray<TBatch> function: TArray<TScalarBatch>
begin begin
var fs := TFileStream.Create(capFileName, fmOpenRead or fmShareDenyWrite); var fs := TFileStream.Create(capFileName, fmOpenRead or fmShareDenyWrite);
try try
@@ -443,7 +533,7 @@ begin
end; end;
end; end;
function TScalarRecordFeed.ReadCompressedData(const InputStream: TStream): TArray<TBatch>; function TScalarBatchLoader.ReadCompressedData(const InputStream: TStream): TArray<TScalarBatch>;
var var
decompressionStream: TStream; decompressionStream: TStream;
localHeader: TZipHeader; localHeader: TZipHeader;
@@ -489,11 +579,9 @@ begin
end; end;
end; end;
function TScalarRecordFeed.ReadUncompressedData(const InputStream: TStream): TArray<TBatch>; function TScalarBatchLoader.ReadUncompressedData(const InputStream: TStream): TArray<TScalarBatch>;
var var
fileSize, totalRecords: Int64; fileSize, totalRecords: Int64;
fieldsPerRecord: Integer;
internalRecordSize, sourceRecordSize: Integer;
numBatches, batchIdx, k: Integer; numBatches, batchIdx, k: Integer;
bytesToRead, valuesToWrite: Integer; bytesToRead, valuesToWrite: Integer;
tempBuffer: TBytes; tempBuffer: TBytes;
@@ -504,61 +592,46 @@ begin
InputStream.Position := 0; InputStream.Position := 0;
fileSize := InputStream.Size; fileSize := InputStream.Size;
fieldsPerRecord := FDef.Count; if (fileSize = 0) or ((fileSize mod FRecordSize) <> 0) then
internalRecordSize := fieldsPerRecord * sizeof(TScalar.TValue);
if (FImportRecordSize > 0) and Assigned(FImportConverter) then
sourceRecordSize := FImportRecordSize
else
sourceRecordSize := internalRecordSize;
if (fileSize = 0) or ((fileSize mod sourceRecordSize) <> 0) then
exit; exit;
totalRecords := fileSize div sourceRecordSize; totalRecords := fileSize div FRecordSize;
if totalRecords = 0 then if totalRecords = 0 then
exit; exit;
numBatches := (totalRecords + BatchSize - 1) div BatchSize; numBatches := (totalRecords + BatchSize - 1) div BatchSize;
SetLength(Result, numBatches); SetLength(Result, numBatches);
if sourceRecordSize <> internalRecordSize then // Buffer for reading raw data chunks before conversion
SetLength(tempBuffer, BatchSize * sourceRecordSize); SetLength(tempBuffer, BatchSize * FRecordSize);
for batchIdx := 0 to numBatches - 1 do for batchIdx := 0 to numBatches - 1 do
begin begin
var recordsInBatch := Min(BatchSize, totalRecords - (batchIdx * BatchSize)); var recordsInBatch := Min(BatchSize, totalRecords - (batchIdx * BatchSize));
Result[batchIdx].RecCount := recordsInBatch; Result[batchIdx].RecCount := recordsInBatch;
valuesToWrite := recordsInBatch * fieldsPerRecord;
// FRecordSize determines both allocation count and input byte stride
valuesToWrite := recordsInBatch * FRecordSize;
SetLength(Result[batchIdx].Data, valuesToWrite); SetLength(Result[batchIdx].Data, valuesToWrite);
if sourceRecordSize = internalRecordSize then bytesToRead := recordsInBatch * FRecordSize;
begin if InputStream.Read(tempBuffer, 0, bytesToRead) <> bytesToRead then
bytesToRead := valuesToWrite * sizeof(TScalar.TValue); raise EReadError.Create('Unexpected end of stream');
if InputStream.Read(Result[batchIdx].Data[0], bytesToRead) <> bytesToRead then
raise EReadError.Create('Unexpected end of stream');
end
else
begin
bytesToRead := recordsInBatch * sourceRecordSize;
if InputStream.Read(tempBuffer, 0, bytesToRead) <> bytesToRead then
raise EReadError.Create('Unexpected end of stream');
pSrc := @tempBuffer[0]; pSrc := @tempBuffer[0];
pDst := @Result[batchIdx].Data[0]; pDst := @Result[batchIdx].Data[0];
for k := 0 to recordsInBatch - 1 do // Always use the converter to transform raw bytes to TValues
begin for k := 0 to recordsInBatch - 1 do
FImportConverter(pSrc, pDst); begin
Inc(pSrc, sourceRecordSize); FConverter(pSrc, pDst);
Inc(pDst, fieldsPerRecord); Inc(pSrc, FRecordSize);
end; Inc(pDst, FRecordSize);
end; end;
end; end;
end; end;
function TScalarRecordFeed.ProcessData(const Symbol: String; const Terminated: TState): TState; function TScalarBatchLoader.ProcessData(const Symbol: String; const Terminated: TState): TState;
begin begin
var cTerminated := Terminated; var cTerminated := Terminated;
var cSymbol := Symbol; var cSymbol := Symbol;
@@ -583,10 +656,10 @@ begin
); );
end; end;
function TScalarRecordFeed.ProcessFile( function TScalarBatchLoader.ProcessFile(
const Files: TArray<TDataFile>; const Files: TArray<TDataFile>;
Index: Integer; Index: Integer;
const DataFile: TFuture<TArray<TBatch>>; const DataFile: TFuture<TArray<TScalarBatch>>;
const Terminated: TState const Terminated: TState
): TState; ): TState;
begin begin
@@ -600,19 +673,19 @@ begin
// Prefetch logic: Prepare the next file immediately (if available) // Prefetch logic: Prepare the next file immediately (if available)
var nextIndex := Index + 1; var nextIndex := Index + 1;
var nextFuture: TFuture<TArray<TBatch>>; var nextFuture: TFuture<TArray<TScalarBatch>>;
if nextIndex <= High(Files) then if nextIndex <= High(Files) then
nextFuture := LoadDataFile(Files[nextIndex]) nextFuture := LoadDataFile(Files[nextIndex])
else else
nextFuture := TFuture<TArray<TBatch>>.Construct(TArray<TBatch>(nil)); // End of stream nextFuture := TFuture<TArray<TScalarBatch>>.Construct(TArray<TScalarBatch>(nil)); // End of stream
var cTerminated := Terminated; var cTerminated := Terminated;
// Process current data, then recurse to next index // Process current data, then recurse to next index
Result := Result :=
DataFile.Chain( DataFile.Chain(
function(const Batches: TArray<TBatch>): TState function(const Batches: TArray<TScalarBatch>): TState
begin begin
var done := ProcessChunks(Batches, Terminated); var done := ProcessChunks(Batches, Terminated);
@@ -623,39 +696,20 @@ begin
); );
end; end;
function TScalarRecordFeed.ProcessChunks(const Batches: TArray<TBatch>; Terminated: TState): TState; function TScalarBatchLoader.ProcessChunks(const Batches: TArray<TScalarBatch>; Terminated: TState): TState;
begin begin
var callProcess := var callProcess :=
function(Idx: Integer): TFunc<TState> function(Idx: Integer): TFunc<TState>
begin begin
var cData := Batches[Idx].Data; var cBatch := Batches[Idx];
var cCount := Batches[Idx].RecCount;
Result := Result :=
function: TState function: TState
begin begin
if not Terminated.IsSet then if not Terminated.IsSet then
begin begin
if (Length(cData) > 0) and (cCount > 0) then // Broadcast the raw batch.
begin FBatchProducer.Broadcast(cBatch);
var Stride := Length(cData) div cCount;
var pCursor: TScalar.PValue := @cData[0];
// Initialize reused View object (Cursor Pattern)
// This object implements IScalarRecord and is updated for each tick.
var ViewObj := TScalarRecordView.Create(FDef);
var View := ViewObj as IScalarRecord;
for var i := 0 to cCount - 1 do
begin
ViewObj.SetData(pCursor);
// Broadcast to all listeners
FTicker.Broadcast(View);
Inc(pCursor, Stride);
end;
end;
Result := TState.Null; Result := TState.Null;
end; end;
end; end;
-22
View File
@@ -23,9 +23,6 @@ type
[TestCase('Float_Negative', '-3.1415926535,Float')] [TestCase('Float_Negative', '-3.1415926535,Float')]
procedure TestScalarCreation(const AValueStr: string; AExpectedKind: TScalar.TKind); procedure TestScalarCreation(const AValueStr: string; AExpectedKind: TScalar.TKind);
// More detailed tests for complex POD types.
[Test]
procedure TestScalarArray;
[Test] [Test]
procedure TestScalarTuple; procedure TestScalarTuple;
end; end;
@@ -59,25 +56,6 @@ begin
end; end;
end; end;
procedure TTestPOD.TestScalarArray;
var
arr: TScalarArray;
values: TArray<TScalar.TValue>;
begin
// Test with values
values := [TScalar.FromInt64(10).Value, TScalar.FromInt64(20).Value, TScalar.FromInt64(30).Value];
arr := TScalarArray.Create(TScalar.TKind.Ordinal, values);
Assert.AreEqual(TScalar.TKind.Ordinal, arr.Kind, 'Array kind mismatch');
Assert.AreEqual(Int64(3), Length(arr.Items), 'Array length mismatch');
Assert.AreEqual(Int64(20), arr.Items[1].AsInt64, 'Array item content mismatch');
// Test empty
arr := TScalarArray.Create(TScalar.TKind.Float, []);
Assert.AreEqual(TScalar.TKind.Float, arr.Kind, 'Empty array kind mismatch');
Assert.AreEqual(Int64(0), Length(arr.Items), 'Empty array length should be zero');
end;
procedure TTestPOD.TestScalarTuple; procedure TTestPOD.TestScalarTuple;
var var
tuple: TScalarTuple; tuple: TScalarTuple;