Skip to content

Commit d8843c3

Browse files
authored
Merge pull request #62 from visualHFT/improve-snapshots
Refactor OrderBookSnapshot for performance and clarity, utilizing a h…
2 parents 0d8ed22 + c399081 commit d8843c3

11 files changed

Lines changed: 1368 additions & 478 deletions

File tree

App.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private static async Task GCCleanupAsync()
7272

7373
while (true)
7474
{
75-
await Task.Delay(5000);
75+
await Task.Delay(35000);
7676
GC.Collect(0, GCCollectionMode.Forced, false); //force garbage collection
7777
}
7878

ViewModel/vmOrderBook.cs

Lines changed: 43 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ 7. Performance Considerations
117117
using OxyPlot.Series;
118118
using Prism.Mvvm;
119119
using System;
120+
using System.Buffers;
120121
using System.Collections.Generic;
121122
using System.Collections.ObjectModel;
123+
using System.Collections.Specialized;
122124
using System.Linq;
123125
using System.Windows;
124126
using System.Windows.Threading;
@@ -127,6 +129,7 @@ 7. Performance Considerations
127129
using VisualHFT.Enums;
128130
using VisualHFT.Helpers;
129131
using VisualHFT.Model;
132+
using Windows.Foundation.Collections;
130133
using AxisPosition = OxyPlot.Axes.AxisPosition;
131134

132135

@@ -140,11 +143,6 @@ public class vmOrderBook : BindableBase, IDisposable
140143
private readonly TimeSpan _MIN_UI_REFRESH_TS = TimeSpan.FromMilliseconds(60); //For the UI: do not allow less than this, since it is not noticeble for human eye
141144

142145

143-
private static class OrderBookSnapshotPool
144-
{
145-
public static readonly CustomObjectPool<OrderBookSnapshot> Instance = new
146-
CustomObjectPool<OrderBookSnapshot>(maxPoolSize: _MAX_CHART_POINTS + (int)(_MAX_CHART_POINTS*1.1));
147-
}
148146
private static class ScatterPointsPool
149147
{
150148
public static readonly CustomObjectPool<OxyPlot.Series.ScatterPoint> Instance =
@@ -165,11 +163,9 @@ private static class ScatterPointsListPool
165163
private string _selectedSymbol;
166164
private VisualHFT.ViewModel.Model.Provider _selectedProvider = null;
167165
private AggregationLevel _aggregationLevelSelection;
168-
169166
private List<BookItem> _bidsGrid;
170167
private List<BookItem> _asksGrid;
171168
private CachedCollection<BookItem> _depthGrid;
172-
173169
private ObservableCollection<VisualHFT.ViewModel.Model.Provider> _providers;
174170

175171
private BookItem _AskTOB = new BookItem();
@@ -568,6 +564,7 @@ private void uiUpdaterAction()
568564
RaisePropertyChanged(nameof(Spread));
569565
RaisePropertyChanged(nameof(LOBImbalanceValue));
570566

567+
571568
RaisePropertyChanged(nameof(Bids));
572569
RaisePropertyChanged(nameof(Asks));
573570
RaisePropertyChanged(nameof(Depth));
@@ -630,40 +627,33 @@ private void LIMITORDERBOOK_OnDataReceived(OrderBook e)
630627

631628

632629
e.CalculateMetrics();
633-
OrderBookSnapshot snapshot = OrderBookSnapshotPool.Instance.Get();
630+
// ✅ CHANGED: Use struct factory method instead of pool
631+
var snapshot = OrderBookSnapshot.Create();
634632
// Initialize its state based on the master OrderBook.
635633
snapshot.UpdateFrom(e);
636634
// Enqueue for processing.
637635
_QUEUE.Add(snapshot);
638636
}
639637
private void _AGGREGATED_LOB_OnRemoving(object? sender, OrderBookSnapshot e)
640638
{
641-
// Perform cleanup BEFORE returning the object to the pool
639+
// Perform cleanup BEFORE disposing the arrays
642640
lock (RealTimeSpreadModel.SyncRoot)
643641
RemoveLastPointToSpreadChart();
644642
lock (RealTimePricePlotModel.SyncRoot)
645643
RemoveLastPointsToScatterChart();
646644

647-
// NOW it is safe to return the object to the pool
648-
OrderBookSnapshotPool.Instance.Return(e);
645+
// ✅ CHANGED: Dispose to return arrays to pool (idempotent, safe to call multiple times)
646+
e.Dispose();
647+
648+
// ❌ REMOVED: No longer needed - struct doesn't pool itself
649+
// OrderBookSnapshotPool.Instance.Return(e);
649650
}
650651
private void _AGGREGATED_LOB_OnRemoved(object? sender, int index)
651652
{
652-
//for current snapshot, make sure to return to the pool
653-
if (index == -1)
654-
OrderBookSnapshotPool.Instance.Reset(); //reset the entire pool
653+
// ❌ REMOVED: Struct snapshots don't need pool reset
655654

656-
//remove last points on the chart
657-
if (index == 0) //make sure the item is the last
658-
{
659-
// This logic is now moved to _AGGREGATED_LOB_OnRemoving
660-
/*
661-
lock (RealTimeSpreadModel.SyncRoot)
662-
RemoveLastPointToSpreadChart();
663-
lock (RealTimePricePlotModel.SyncRoot)
664-
RemoveLastPointsToScatterChart();
665-
*/
666-
}
655+
// Note: Cleanup is now handled in _AGGREGATED_LOB_OnRemoving via Dispose()
656+
// which is called BEFORE the item is removed from the collection
667657
}
668658

669659

@@ -700,7 +690,8 @@ private void QUEUE_onReadAction(OrderBookSnapshot ob)
700690

701691
if (!addedOK)
702692
{
703-
OrderBookSnapshotPool.Instance.Return(ob);
693+
// ✅ CHANGED: Dispose snapshot (returns arrays to pool) if not added
694+
ob.Dispose();
704695
return;
705696
}
706697

@@ -724,13 +715,16 @@ private void QUEUE_onReadAction(OrderBookSnapshot ob)
724715
}
725716

726717
// ✅ PHASE 4: Create scatter points OUTSIDE ANY LOCKS
718+
// Convert spans to arrays for filtering (temporary allocations, but minimal)
719+
var bidsArray = lobItemToDisplay.Bids.ToArray();
720+
var asksArray = lobItemToDisplay.Asks.ToArray();
721+
727722
var bidLevelPoints = ToScatterPointsLevels(
728-
lobItemToDisplay.Bids.Where(x => x.Price >= _MidPoint * 0.99),
723+
bidsArray.Where(x => x.Price >= _MidPoint * 0.99).ToArray(),
729724
sharedTS);
730725
var askLevelPoints = ToScatterPointsLevels(
731-
lobItemToDisplay.Asks.Where(x => x.Price <= _MidPoint * 1.01),
726+
asksArray.Where(x => x.Price <= _MidPoint * 1.01).ToArray(),
732727
sharedTS);
733-
734728
try
735729
{
736730
// ✅ PHASE 5: Update scatter chart (Level 3 lock)
@@ -768,15 +762,15 @@ private void QUEUE_onErrorAction(Exception ex)
768762

769763
private DataPoint? ToDataPointBestBid(OrderBookSnapshot? lob, double sharedTS)
770764
{
771-
if (lob != null && lob.Bids != null && lob.Bids.Count > 0 && lob.Bids[0].Price.HasValue)
772-
return new DataPoint(sharedTS, lob.Bids[0].Price.Value);
765+
if (lob.HasValue && lob.Value.Bids.Length > 0 && lob.Value.Bids[0]?.Price.HasValue == true)
766+
return new DataPoint(sharedTS, lob.Value.Bids[0].Price.Value);
773767
else
774768
return null;
775769
}
776770
private DataPoint? ToDataPointBestAsk(OrderBookSnapshot? lob, double sharedTS)
777771
{
778-
if (lob != null && lob.Asks != null && lob.Asks.Count > 0 && lob.Asks[0].Price.HasValue)
779-
return new DataPoint(sharedTS, lob.Asks[0].Price.Value);
772+
if (lob.HasValue && lob.Value.Asks.Length > 0 && lob.Value.Asks[0]?.Price.HasValue == true)
773+
return new DataPoint(sharedTS, lob.Value.Asks[0].Price.Value);
780774
else
781775
return null;
782776
}
@@ -789,10 +783,10 @@ private DataPoint ToDataPointSpread(OrderBookSnapshot lob, double sharedTS)
789783
return new DataPoint(sharedTS, lob.Spread);
790784
}
791785
private List<OxyPlot.Series.ScatterPoint> ToScatterPointsLevels(
792-
IEnumerable<BookItem> lobList,
786+
ReadOnlySpan<BookItem> lobSpan,
793787
double sharedTS)
794788
{
795-
if (lobList == null || !lobList.Any())
789+
if (lobSpan.IsEmpty)
796790
{
797791
var emptyList = ScatterPointsListPool.Instance.Get();
798792
emptyList.Clear();
@@ -803,6 +797,9 @@ private DataPoint ToDataPointSpread(OrderBookSnapshot lob, double sharedTS)
803797
var scatterPoints = ScatterPointsListPool.Instance.Get();
804798
scatterPoints.Clear();
805799

800+
// Convert span to array for LINQ operations (temporary allocation)
801+
var lobList = lobSpan.ToArray();
802+
806803
// Size normalization logic
807804
_minScatterBubbleSize = Math.Min(_minScatterBubbleSize, lobList.Min(x => x.Size.Value));
808805
_maxScatterBubbleSize = Math.Max(_maxScatterBubbleSize, lobList.Max(x => x.Size.Value));
@@ -829,9 +826,13 @@ private DataPoint ToDataPointSpread(OrderBookSnapshot lob, double sharedTS)
829826
}
830827
return scatterPoints; // ← List will be returned to pool, ScatterPoints will live in chart
831828
}
832-
private IEnumerable<DataPoint> ToDataPointsCumulativeVolume(List<BookItem> lobList, double sharedTS)
829+
private IEnumerable<DataPoint> ToDataPointsCumulativeVolume(ReadOnlySpan<BookItem> lobSpan, double sharedTS)
833830
{
834-
var retItems = new List<DataPoint>(lobList.Count);
831+
if (lobSpan.IsEmpty)
832+
return Enumerable.Empty<DataPoint>();
833+
834+
var lobList = lobSpan.ToArray(); // Temporary conversion for processing
835+
var retItems = new List<DataPoint>(lobList.Length);
835836
double cumulativeVol = 0;
836837

837838
foreach (var level in lobList)
@@ -1085,7 +1086,6 @@ private void Clear()
10851086
_AGGREGATED_LOB.OnRemoving += _AGGREGATED_LOB_OnRemoving;
10861087
}
10871088

1088-
OrderBookSnapshotPool.Instance.Reset(); //reset the entire pool
10891089
ScatterPointsPool.Instance.Reset();
10901090

10911091
Dispatcher.CurrentDispatcher.BeginInvoke(() =>
@@ -1110,7 +1110,6 @@ private void Clear()
11101110

11111111
}
11121112

1113-
11141113
/// <summary>
11151114
/// Bids the ask grid update.
11161115
/// Update our internal lists trying to re-use the current items on the list.
@@ -1119,29 +1118,16 @@ private void Clear()
11191118
/// <param name="orderBook">The order book.</param>
11201119
private void BidAskGridUpdate(OrderBookSnapshot orderBook)
11211120
{
1122-
if (orderBook == null)
1123-
return;
1124-
11251121
GridListUpdate(_asksGrid, orderBook.Asks);
11261122
GridListUpdate(_bidsGrid, orderBook.Bids);
1127-
1128-
//commented out for now
1129-
/*if (_asksGrid != null && _bidsGrid != null)
1130-
{
1131-
_depthGrid.Clear();
1132-
foreach (var item in _asksGrid)
1133-
_depthGrid.Add(item);
1134-
foreach (var item in _bidsGrid)
1135-
_depthGrid.Add(item);
1136-
}*/
11371123
}
11381124

1139-
private void GridListUpdate(List<BookItem> currentList, List<BookItem> newList)
1125+
private void GridListUpdate(List<BookItem> currentList, ReadOnlySpan<BookItem> newList)
11401126
{
11411127
// Update existing items and add/remove as needed
1142-
for (int i = 0; i < Math.Max(currentList.Count, newList.Count); i++)
1128+
for (int i = 0; i < Math.Max(currentList.Count, newList.Length); i++)
11431129
{
1144-
if (i < newList.Count)
1130+
if (i < newList.Length)
11451131
{
11461132
if (i < currentList.Count)
11471133
UpdateBookItem(currentList[i], newList[i]); // Update existing item
@@ -1271,7 +1257,7 @@ public IReadOnlyList<BookItem> Bids
12711257
public PlotModel RealTimePricePlotModel { get; set; }
12721258
public PlotModel RealTimeSpreadModel { get; set; }
12731259
public PlotModel CummulativeBidsChartModel { get; set; }
1274-
public PlotModel CummulativeAsksChartModel { get; set; }
1260+
public PlotModel CummulativeAsksChartModel { get; }
12751261

12761262

12771263
public int SwitchView
@@ -1299,11 +1285,10 @@ protected virtual void Dispose(bool disposing)
12991285
_dialogs = null;
13001286
_realTimeTrades?.Clear();
13011287
_depthGrid?.Clear();
1288+
_providers?.Clear();
13021289
_bidsGrid?.Clear();
13031290
_asksGrid?.Clear();
1304-
_providers?.Clear();
13051291
ScatterPointsPool.Instance.Dispose();
1306-
OrderBookSnapshotPool.Instance.Dispose();
13071292
ScatterPointsListPool.Instance.Dispose();
13081293
_QUEUE?.Dispose();
13091294

VisualHFT.Commons/Helpers/AggregatedCollection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313
namespace VisualHFT.Helpers
1414
{
15-
public class AggregatedCollection<T> : IDisposable, IEnumerable<T> where T : class, new()
15+
public class AggregatedCollection<T> : IDisposable, IEnumerable<T> where T : new()
1616
{
1717
private bool _disposed = false; // to track whether the object has been disposed
1818
private TimeSpan _aggregationSpan;

VisualHFT.Commons/Helpers/CachedCollection.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
using System.Collections;
2-
using System.Collections.ObjectModel;
3-
using VisualHFT.Commons.Model;
4-
using VisualHFT.Commons.Pools;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System;
2+
using System.Runtime.CompilerServices;
83

94
namespace VisualHFT.Helpers
105
{
@@ -321,5 +316,27 @@ public void TruncateItemsAfterPosition(int v)
321316
// If v is greater than or equal to the last index, nothing to truncate.
322317
}
323318
}
319+
320+
321+
/// <summary>
322+
/// Returns a read-only span view of the internal list WITHOUT allocating.
323+
/// Uses CollectionsMarshal for zero-copy access to List's internal array.
324+
/// WARNING: Span is only valid while the lock is held!
325+
/// </summary>
326+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
327+
public ReadOnlySpan<T> AsSpan(int maxCount = int.MaxValue)
328+
{
329+
// ⚠️ CALLER MUST HOLD _lock!
330+
// This method does NOT lock internally to avoid nested locks
331+
332+
if (_internalList == null || _internalList.Count == 0)
333+
return ReadOnlySpan<T>.Empty;
334+
335+
// Use CollectionsMarshal to access List's internal array (ZERO COPY!)
336+
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_internalList);
337+
338+
int count = Math.Min(span.Length, maxCount);
339+
return span.Slice(0, count);
340+
}
324341
}
325342
}

0 commit comments

Comments
 (0)