Pipe Optimization
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stream & Pipe Optimization
|
||||||
|
|
||||||
|
### 1. Global Series Deduplication (The "Registry" Pattern)
|
||||||
|
|
||||||
|
Die aktuelle Architektur erzeugt für jeden Pipe-Eingang einen eigenen Akkumulator. Bei komplexen Graphen mit identischen Datenquellen führt dies zu redundantem Speicherverbrauch.
|
||||||
|
|
||||||
|
* **Konzept:** Einführung einer **Global Series Registry**, die als Mediator zwischen `IStream` und `TPipeSource` fungiert.
|
||||||
|
* **Mechanismus:** Anstatt eine `TScalarSeries` privat zu instanziieren, fordert die `TPipeSource` eine Serie über einen Composite-Key `(IStream, IKeyword)` an.
|
||||||
|
* **Vorteil:** Wenn zehn Pipes den `Close`-Preis desselben Tickers beobachten, existiert im gesamten Arbeitsspeicher nur eine einzige Instanz der Daten-Historie.
|
||||||
|
* **Lookback-Management:** Die Registry überwacht die Anforderungen aller Konsumenten und stellt sicher, dass die geteilte Serie immer den **maximalen Lookback** aller beteiligten Pipes vorhält.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Local State Optimization (Ring-Buffer Series)
|
||||||
|
|
||||||
|
Für Berechnungen innerhalb der Lambda-Funktion (z.B. kurzfristige Delta-Vergleiche) sind die auf Chunks basierenden `TScalarSeries` aufgrund ihrer Verwaltungs-Overheads ineffizient.
|
||||||
|
|
||||||
|
* **Konzept:** Implementierung spezialisierter **Local Ring-Buffer**, die für ultrakurze Lookbacks (z.B. ) optimiert sind.
|
||||||
|
* **Mechanismus:** Ein statisch allozierter Speicherblock mit `truncate-on-add`-Logik. Im Gegensatz zu Chunks gibt es hier keine Heap-Fragmentation durch dynamisches Wachsen.
|
||||||
|
* **Scope:** Diese Strukturen sind "Lambda-Local". Sie werden nicht über die Registry geteilt, sondern dienen als privater, blitzschneller "Scratchpad-Speicher" für zustandsbehaftete Berechnungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Signal Transmission & Pressure Relief
|
||||||
|
|
||||||
|
Das ständige Allokieren von `TArray<TScalar.TValue>` in der `Emit`-Methode erzeugt bei hohen Tick-Raten signifikanten Druck auf den Memory-Manager und das Reference-Counting.
|
||||||
|
|
||||||
|
* **Konzept:** **Allocation Pooling** oder **Buffer Slicing** für Signal-Payloads.
|
||||||
|
* **Mechanismus:** Einführung eines `TValueBufferPool`. Streams leihen sich ein Array für den `Emit`-Vorgang aus. Sobald der letzte Observer das Signal verarbeitet hat, kehrt das Array in den Pool zurück.
|
||||||
|
* **Alternative:** Nutzung von `Record`-basierten Memory-Slices, um Kopier-Operationen beim Erzeugen des Signals gänzlich zu eliminieren (**Zero-Copy**).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Runtime Adaptability (Dynamic Lookback)
|
||||||
|
|
||||||
|
Aktuell ist der Lookback einer Serie bei der Initialisierung festgeschrieben. In einem dynamischen System müssen Pipes jedoch zur Laufzeit hinzugeschaltet werden können.
|
||||||
|
|
||||||
|
* **Konzept:** **Elastic Series Buffers**.
|
||||||
|
* **Mechanismus:** Die `TScalarSeries` erhält die Fähigkeit, ihre Chunk-Kapazität dynamisch nach oben zu korrigieren, ohne die Integrität der bestehenden Indizes zu verletzen.
|
||||||
|
* **Motivation:** Dies ermöglicht "Hot-Swapping" von Logik-Komponenten. Eine neue Analyse-Pipe kann sich in einen laufenden Stream einklinken und eine Erweiterung der Historie anfordern, ohne dass das System neu gestartet werden muss.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Computational Economy (Lazy/Dirty Logic)
|
||||||
|
|
||||||
|
In tiefen Graphen werden oft Pipes getriggert, deren Eingangsdaten sich zwar im Cycle geändert haben, deren für die Berechnung relevante Werte aber identisch geblieben sind.
|
||||||
|
|
||||||
|
* **Konzept:** **Change-Detection & Lazy Evaluation**.
|
||||||
|
* **Mechanismus:** Jedes Signal erhält eine Versionierung. Pipes führen ihre Lambda nur aus, wenn sich die Versionen der Eingangs-Serien tatsächlich geändert haben oder wenn ein "Dirty"-Flag gesetzt ist.
|
||||||
|
* **Ziel:** Massive Einsparung von CPU-Zyklen in Szenarien, in denen viele Pipes auf hochfrequenten Streams hängen, aber nur selten Schwellenwerte überschritten werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Zusammenfassung der Architektur-Ziele
|
||||||
|
|
||||||
|
| Fokus | Methode | Resultat |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Memory** | Global Registry | Eliminierung redundanter Daten-Historien. |
|
||||||
|
| **Latency** | Ring-Buffers | Beschleunigung lokaler Lambda-Berechnungen. |
|
||||||
|
| **Throughput** | Buffer Pooling | Minimierung von GC-Pausen und Allokations-Overhead. |
|
||||||
|
| **Agility** | Dynamic Lookback | Unterstützung von On-the-fly Rekonfiguration. |
|
||||||
|
|
||||||
|
---
|
||||||
@@ -636,7 +636,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
//TODO Lookback not evaluated in script!
|
//TODO Lookback not evaluated in script!
|
||||||
Result := TPipeStream.Create(config, outputDef, sources, pipeAdapter, 1000);
|
Result := TPipeStream.Create(config, outputDef, sources, pipeAdapter, 1000, 1);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -17,16 +17,17 @@ type
|
|||||||
// TStreamSignal represents the data packet traveling through the graph.
|
// TStreamSignal represents the data packet traveling through the graph.
|
||||||
// It carries the CycleID for synchronization and the actual data payload.
|
// It carries the CycleID for synchronization and the actual data payload.
|
||||||
TStreamSignal = record
|
TStreamSignal = record
|
||||||
strict private
|
private
|
||||||
FCycleID: Int64;
|
FCycleID: Int64;
|
||||||
FKind: TSignalKind;
|
FData: TScalar.PValue;
|
||||||
FData: TArray<TScalar.TValue>;
|
function GetFields(Idx: Integer): TScalar.TValue; inline;
|
||||||
|
function GetKind: TSignalKind; inline;
|
||||||
public
|
public
|
||||||
constructor Create(AKind: TSignalKind; ACycleID: Int64; const AData: TArray<TScalar.TValue> = nil);
|
constructor Create(ACycleID: Int64; const AData: TScalar.PValue);
|
||||||
function ToString: string;
|
function ToString: string;
|
||||||
|
property Fields[Idx: Integer]: TScalar.TValue read GetFields;
|
||||||
property CycleID: Int64 read FCycleID;
|
property CycleID: Int64 read FCycleID;
|
||||||
property Kind: TSignalKind read FKind;
|
property Kind: TSignalKind read GetKind;
|
||||||
property Data: TArray<TScalar.TValue> read FData;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TSignalProc = reference to procedure(const Signal: TStreamSignal);
|
TSignalProc = reference to procedure(const Signal: TStreamSignal);
|
||||||
@@ -55,7 +56,7 @@ type
|
|||||||
function GetDef: IScalarRecordDefinition;
|
function GetDef: IScalarRecordDefinition;
|
||||||
protected
|
protected
|
||||||
{ Broadcasts data to all subscribers. Stateless. }
|
{ Broadcasts data to all subscribers. Stateless. }
|
||||||
procedure Emit(const Value: array of TScalar.TValue; ACycleID: Int64);
|
procedure Emit(CycleID: Int64; Values: TScalar.PValue);
|
||||||
public
|
public
|
||||||
constructor Create(const ADef: IScalarRecordDefinition);
|
constructor Create(const ADef: IScalarRecordDefinition);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
@@ -83,6 +84,7 @@ type
|
|||||||
public
|
public
|
||||||
type
|
type
|
||||||
TSignalProc = reference to procedure(const Signal: TStreamSignal);
|
TSignalProc = reference to procedure(const Signal: TStreamSignal);
|
||||||
|
|
||||||
strict private
|
strict private
|
||||||
FSource: IStream;
|
FSource: IStream;
|
||||||
FTag: TSubscriptionTag;
|
FTag: TSubscriptionTag;
|
||||||
@@ -90,21 +92,21 @@ type
|
|||||||
FFieldIndex: Integer;
|
FFieldIndex: Integer;
|
||||||
FData: IWriteableSeries;
|
FData: IWriteableSeries;
|
||||||
FLastSeenCycle: Int64;
|
FLastSeenCycle: Int64;
|
||||||
FLookback: Int64;
|
FMaxLookback: Int64;
|
||||||
|
|
||||||
{ ISeries implementation }
|
{ ISeries implementation }
|
||||||
function GetCount: Int64;
|
function GetCount: Int64;
|
||||||
function GetItems(Idx: Integer): TScalar;
|
function GetItems(Idx: Integer): TScalar;
|
||||||
function GetTotalCount: Int64;
|
function GetTotalCount: Int64;
|
||||||
private
|
|
||||||
procedure HandleSignal(const Signal: TStreamSignal);
|
procedure HandleSignal(const Signal: TStreamSignal);
|
||||||
protected
|
|
||||||
|
private
|
||||||
procedure Detach;
|
procedure Detach;
|
||||||
|
|
||||||
public
|
public
|
||||||
constructor Create(const ASource: IStream; const AField: IKeyword; ALookback: Int64; const AOnSignal: TSignalProc);
|
constructor Create(const ASource: IStream; const AField: IKeyword; AMaxLookback: Int64; const AOnSignal: TSignalProc);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
|
|
||||||
function IsReady(RequiredLookback: Int64): Boolean;
|
|
||||||
property LastSeenCycle: Int64 read FLastSeenCycle;
|
property LastSeenCycle: Int64 read FLastSeenCycle;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -114,11 +116,13 @@ type
|
|||||||
public
|
public
|
||||||
type
|
type
|
||||||
TPipeLambda = reference to function(const Sources: array of ISeries; out Results: array of TScalar.TValue): Boolean;
|
TPipeLambda = reference to function(const Sources: array of ISeries; out Results: array of TScalar.TValue): Boolean;
|
||||||
|
|
||||||
strict private
|
strict private
|
||||||
FSources: TArray<ISeries>;
|
FSources: TArray<ISeries>;
|
||||||
FResults: TArray<TScalar.TValue>;
|
FResults: TArray<TScalar.TValue>;
|
||||||
FLastFiredCycleID: Int64;
|
FLastFiredCycleID: Int64;
|
||||||
FLookback: Int64;
|
FMaxLookback: Int64;
|
||||||
|
FRequiredLookback: Int64;
|
||||||
FLambda: TPipeLambda;
|
FLambda: TPipeLambda;
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -130,27 +134,45 @@ type
|
|||||||
const ADef: IScalarRecordDefinition;
|
const ADef: IScalarRecordDefinition;
|
||||||
const ASources: TArray<IStream>;
|
const ASources: TArray<IStream>;
|
||||||
const ALambda: TPipeLambda;
|
const ALambda: TPipeLambda;
|
||||||
ALookback: Int64
|
AMaxLookback, ARequiredLookback: Int64
|
||||||
);
|
);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
property Lookback: Int64 read FLookback;
|
property MaxLookback: Int64 read FMaxLookback;
|
||||||
|
property RequiredLookback: Int64 read FRequiredLookback;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
{ TStreamSignal }
|
{ TStreamSignal }
|
||||||
|
|
||||||
constructor TStreamSignal.Create(AKind: TSignalKind; ACycleID: Int64; const AData: TArray<TScalar.TValue>);
|
constructor TStreamSignal.Create(ACycleID: Int64; const AData: TScalar.PValue);
|
||||||
begin
|
begin
|
||||||
FKind := AKind;
|
|
||||||
FCycleID := ACycleID;
|
FCycleID := ACycleID;
|
||||||
FData := AData;
|
FData := AData;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TStreamSignal.GetFields(Idx: Integer): TScalar.TValue;
|
||||||
|
var
|
||||||
|
P: TScalar.PValue;
|
||||||
|
begin
|
||||||
|
Assert(FData <> nil, 'Heartbeat does not contain data');
|
||||||
|
P := FData;
|
||||||
|
inc(P, Idx);
|
||||||
|
Result := P^;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TStreamSignal.GetKind: TSignalKind;
|
||||||
|
begin
|
||||||
|
if FData <> nil then
|
||||||
|
Result := skData
|
||||||
|
else
|
||||||
|
Result := skHeartbeat;
|
||||||
|
end;
|
||||||
|
|
||||||
function TStreamSignal.ToString: string;
|
function TStreamSignal.ToString: string;
|
||||||
begin
|
begin
|
||||||
if Kind = skData then
|
if Kind = skData then
|
||||||
Result := Format('Signal(Data, #%d, Fields: %d)', [CycleID, Length(Data)])
|
Result := Format('Signal(Data, #%d)', [CycleID])
|
||||||
else
|
else
|
||||||
Result := Format('Signal(Heartbeat, #%d)', [CycleID]);
|
Result := Format('Signal(Heartbeat, #%d)', [CycleID]);
|
||||||
end;
|
end;
|
||||||
@@ -197,19 +219,11 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TCustomDataStream.Emit(const Value: array of TScalar.TValue; ACycleID: Int64);
|
procedure TCustomDataStream.Emit(CycleID: Int64; Values: TScalar.PValue);
|
||||||
var
|
var
|
||||||
LSignal: TStreamSignal;
|
LSignal: TStreamSignal;
|
||||||
LData: TArray<TScalar.TValue>;
|
|
||||||
I: Integer;
|
|
||||||
begin
|
begin
|
||||||
// Convert open array to dynamic array for signal payload
|
LSignal.Create(CycleID, Values);
|
||||||
SetLength(LData, Length(Value));
|
|
||||||
for I := 0 to High(Value) do
|
|
||||||
LData[I] := Value[I];
|
|
||||||
|
|
||||||
LSignal := TStreamSignal.Create(skData, ACycleID, LData);
|
|
||||||
|
|
||||||
FObservers.Lock;
|
FObservers.Lock;
|
||||||
try
|
try
|
||||||
FObservers.Notify(
|
FObservers.Notify(
|
||||||
@@ -220,6 +234,7 @@ begin
|
|||||||
end
|
end
|
||||||
);
|
);
|
||||||
finally
|
finally
|
||||||
|
LSignal.FData := nil;
|
||||||
FObservers.Release;
|
FObservers.Release;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -238,17 +253,17 @@ begin
|
|||||||
raise EArgumentException.Create('Push: Data field count mismatch.');
|
raise EArgumentException.Create('Push: Data field count mismatch.');
|
||||||
|
|
||||||
Inc(FLastCycleID);
|
Inc(FLastCycleID);
|
||||||
Emit(RowData, FLastCycleID);
|
Emit(FLastCycleID, @RowData[0]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TPipeSource }
|
{ TPipeSource }
|
||||||
|
|
||||||
constructor TPipeSource.Create(const ASource: IStream; const AField: IKeyword; ALookback: Int64; const AOnSignal: TSignalProc);
|
constructor TPipeSource.Create(const ASource: IStream; const AField: IKeyword; AMaxLookback: Int64; const AOnSignal: TSignalProc);
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
FSource := ASource;
|
FSource := ASource;
|
||||||
FOnSignal := AOnSignal;
|
FOnSignal := AOnSignal;
|
||||||
FLookback := ALookback;
|
FMaxLookback := AMaxLookback;
|
||||||
FLastSeenCycle := -1;
|
FLastSeenCycle := -1;
|
||||||
|
|
||||||
FFieldIndex := ASource.Def.IndexOf(AField);
|
FFieldIndex := ASource.Def.IndexOf(AField);
|
||||||
@@ -278,17 +293,12 @@ begin
|
|||||||
if (Signal.Kind = skData) then
|
if (Signal.Kind = skData) then
|
||||||
begin
|
begin
|
||||||
FLastSeenCycle := Signal.CycleID;
|
FLastSeenCycle := Signal.CycleID;
|
||||||
FData.Add(Signal.Data[FFieldIndex], FLookback);
|
FData.Add(Signal.Fields[FFieldIndex], FMaxLookback);
|
||||||
if Assigned(FOnSignal) then
|
if Assigned(FOnSignal) then
|
||||||
FOnSignal(Signal);
|
FOnSignal(Signal);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TPipeSource.IsReady(RequiredLookback: Int64): Boolean;
|
|
||||||
begin
|
|
||||||
Result := (FLastSeenCycle >= 0) and (FData.Count >= RequiredLookback);
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TPipeSource.GetCount: Int64;
|
function TPipeSource.GetCount: Int64;
|
||||||
begin
|
begin
|
||||||
Result := FData.Count;
|
Result := FData.Count;
|
||||||
@@ -311,14 +321,14 @@ constructor TPipeStream.Create(
|
|||||||
const ADef: IScalarRecordDefinition;
|
const ADef: IScalarRecordDefinition;
|
||||||
const ASources: TArray<IStream>;
|
const ASources: TArray<IStream>;
|
||||||
const ALambda: TPipeLambda;
|
const ALambda: TPipeLambda;
|
||||||
ALookback: Int64
|
AMaxLookback, ARequiredLookback: Int64
|
||||||
);
|
);
|
||||||
var
|
var
|
||||||
i, j, n: Integer;
|
i, j, n: Integer;
|
||||||
begin
|
begin
|
||||||
inherited Create(ADef);
|
inherited Create(ADef);
|
||||||
FLambda := ALambda;
|
FLambda := ALambda;
|
||||||
FLookback := ALookback;
|
FMaxLookback := AMaxLookback;
|
||||||
FLastFiredCycleID := -1;
|
FLastFiredCycleID := -1;
|
||||||
|
|
||||||
// Flatten sources from config
|
// Flatten sources from config
|
||||||
@@ -327,7 +337,7 @@ begin
|
|||||||
inc(n, Length(AConfig[i]));
|
inc(n, Length(AConfig[i]));
|
||||||
|
|
||||||
SetLength(FSources, n);
|
SetLength(FSources, n);
|
||||||
SetLength(FResults, n);
|
SetLength(FResults, ADef.Count);
|
||||||
|
|
||||||
n := 0;
|
n := 0;
|
||||||
for i := 0 to High(ASources) do
|
for i := 0 to High(ASources) do
|
||||||
@@ -339,12 +349,13 @@ begin
|
|||||||
TPipeSource.Create(
|
TPipeSource.Create(
|
||||||
ASources[i],
|
ASources[i],
|
||||||
AConfig[i][j].Key,
|
AConfig[i][j].Key,
|
||||||
FLookback,
|
FMaxLookback,
|
||||||
procedure(const Signal: TStreamSignal) begin CheckBarrierAndFire(Signal.CycleID); end
|
procedure(const Signal: TStreamSignal) begin CheckBarrierAndFire(Signal.CycleID); end
|
||||||
);
|
);
|
||||||
inc(n);
|
inc(n);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
FRequiredLookback := ARequiredLookback;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TPipeStream.Destroy;
|
destructor TPipeStream.Destroy;
|
||||||
@@ -356,12 +367,13 @@ end;
|
|||||||
|
|
||||||
procedure TPipeStream.CheckBarrierAndFire(CurrentCycle: Int64);
|
procedure TPipeStream.CheckBarrierAndFire(CurrentCycle: Int64);
|
||||||
begin
|
begin
|
||||||
const requiredLookback = 10;
|
if FLastFiredCycleID >= CurrentCycle then
|
||||||
|
exit;
|
||||||
|
|
||||||
// 1. Check if all sources reached this cycle AND have enough history (Warm-up)
|
// Check if all sources reached this cycle AND have enough history (Warm-up)
|
||||||
for var src in FSources do
|
for var src in FSources do
|
||||||
begin
|
begin
|
||||||
if src.Count < requiredLookback then
|
if src.Count < FRequiredLookback then
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
var LSource := src as TPipeSource;
|
var LSource := src as TPipeSource;
|
||||||
@@ -369,18 +381,13 @@ begin
|
|||||||
exit;
|
exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if FLastFiredCycleID >= CurrentCycle then
|
|
||||||
Exit;
|
|
||||||
|
|
||||||
FLastFiredCycleID := CurrentCycle;
|
FLastFiredCycleID := CurrentCycle;
|
||||||
|
|
||||||
if Assigned(FLambda) then
|
if Assigned(FLambda) then
|
||||||
begin
|
begin
|
||||||
// Execute transformation on synchronized window
|
// Execute transformation on synchronized window
|
||||||
if FLambda(FSources, FResults) then
|
if FLambda(FSources, FResults) then
|
||||||
begin
|
Emit(FLastFiredCycleID, @FResults[0]);
|
||||||
Emit(FResults, FLastFiredCycleID);
|
|
||||||
end;
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user