Доброго времени суток!
Недавно обнаружил, что Гидра начинает прилично подтормаживать при большом количестве инструментов.
Получил такую ситуацию довольно просто: запустил импорт инструментов у источника Smart (из демо-сервера),
в ходе которого мне прилетело около 30 000 инструментов.
После этого запуск Гидры и переход между вкладками начали тормозить,
а запуска импорта маркет-данных из Smart было не дождаться: он отваливался по таймауту.
Начал копать и обнаружил, что долго выполняется чтение данных из хранилища:
Код
entityRegistry.Securities
Сразу оговорюсь: в качестве хранилища я использую
SQLLite, и, возможно, MS SQL Server тормозит значительно меньше.
0. Понятно, что хотя бы один раз данные об инструментах из базы данных зачитать надо.
Это происходит в классе SecurityStorage:
Код
public SecurityStorage(IEntityRegistry entityRegistry)
{
if (entityRegistry == null)
throw new ArgumentNullException("entityRegistry");
foreach (var security in entityRegistry.Securities)
AddToCache(security);
}
Здесь все зачитанные данные мудро кладутся в кэш.
1. Но вот если посмотреть на класс FinamSecurityStorage, то здесь все уже не так радужно:
Код
public FinamSecurityStorage(ISecurityStorage underlyingStorage, HydraEntityRegistry entityRegistry)
{
if (underlyingStorage == null)
throw new ArgumentNullException("underlyingStorage");
foreach (var security in entityRegistry.Securities)
TryAddToCache(security);
_underlyingStorage = underlyingStorage;
}
}
Т.к. интерфейс ISecurityStorage не позволяет читать данные из кэша underlyingStorage,
для построения особого "финамовского" кэша снова зачитываются данные напрямую из хранилища,
что не очень-то быстро.
Стоит ли вносить методы работы с кэшами в ISecurityStorage или выделить особый интерфейс для этого,
скажем "ICachedSecurityStorage", я сказать не могу - это дело архитекторов StockSharp,
но можно сделать вот такой "костыль":
Код
public FinamSecurityStorage(ISecurityStorage underlyingStorage, HydraEntityRegistry entityRegistry)
{
if (underlyingStorage == null)
throw new ArgumentNullException("underlyingStorage");
_underlyingStorage = underlyingStorage;
if (underlyingStorage is SecurityStorage)
{
foreach (var security in ((SecurityStorage)_underlyingStorage).CachedSecurities)
TryAddToCache(security);
((SecurityStorage)_underlyingStorage).Reloaded += OnReloaded;
}
else
{
foreach (var security in entityRegistry.Securities)
TryAddToCache(security);
}
}
Т.к. в случае Гидры underlyingStorage относится к классу SecurityStorage, этот код быстро код данные из имеющегося кэша.
2. Старт импорта маркет-данных происходит долго из-за следующиего кода в классе Worker:
Код
public bool Start(IEnumerable<VisualSecurity> securities)
{
...
_securities.Clear();
foreach (var group in securities.GroupBy(s => s.TradeInfo.Source)
.Concat(securities.GroupBy(s => s.DepthInfo.Source))
.Concat(securities.GroupBy(s => s.OrderLogInfo.Source))
.Concat(securities.GroupBy(s => s.SecurityChangeInfo.Source))
.Concat(securities.GroupBy(s => s.CandleInfo.Source))
.Where(g => !g.Key.IsEmpty()))
{
_securities.SafeAdd(group.Key).AddRange(group);
}
...
}
Здесь проблема заключается в том, что при каждом вызове securities.GroupBy происходит повторный перебор securities.
За счет того, что построение securities заключается в чтении их напрямую из хранилища здесь мы получаем ударную дозу
из 5 подряд чтений из хранилища, что и приводит к превышению довольно солидного тайм-аута при старте.
Оптимизировать выполнение можно, построив один раз массив и использовав его для группировки:
Код
public bool Start(IEnumerable<VisualSecurity> securities)
{
...
_securities.Clear();
var securitiesArray = securities.ToArray();
foreach (var group in securitiesArray.GroupBy(s => s.TradeInfo.Source)
.Concat(securitiesArray.GroupBy(s => s.DepthInfo.Source))
.Concat(securitiesArray.GroupBy(s => s.OrderLogInfo.Source))
.Concat(securitiesArray.GroupBy(s => s.SecurityChangeInfo.Source))
.Concat(securitiesArray.GroupBy(s => s.CandleInfo.Source))
.Where(g => !g.Key.IsEmpty()))
{
_securities.SafeAdd(group.Key).AddRange(group);
}
...
}
3. Но, собственно, почему в метод Start передается IEnumerable<VisualSecurity>,
который приводит к чтению из хранилища, а не из кэша?
Собственно код в методе StartStopClick класса MainWindow такой (в методе AutoStart аналогичный):
Код
var selectedSecurities = _entityRegistry.Securities.Select(s => s.ToVisualSecurity()).Where(s => s.IsSelected);
if (_worker.Start(selectedSecurities))
{
...
}
else
{
...
}
Здесь тоже можно воспользоваться имеющимся кэшем и переписать построение выбранных интрументов таким образом:
Код
var selectedSecurities = _securityStorage.CachedSecurities
.Where(s => s.IsSelected())
.Select(s => s.ToVisualSecurity())
.ToArray();
В принципе, если бы код был написан сразу так, то оптимизация номер 2 с GroupBy даже не нужна.
Но, имхо, правильнее сделать и то, и другое.
4. Еще невыносимо долго при большом числе инструментов переключаются вкладки.
Код построения всех выбранных интрументов в классе MarketDataSourceControl такой:
Код
private void FillSecurities()
{
var selectedSource = Source.Name;
var storage = HydraEntityRegistry;
SecuritiesCtrl.Securities.Clear();
_selectedSecurities.Clear();
System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
System.Threading.Tasks.Task.Factory.StartNew(
() =>
{
var securities = storage
.Securities
.Select(s => s.ToVisualSecurity()).Where(s => s.IsSelected && (selectedSource == null || (s.TradeInfo.Source == selectedSource ||
s.DepthInfo.Source == selectedSource || s.OrderLogInfo.Source == selectedSource || s.SecurityChangeInfo.Source == selectedSource ||
s.Source == selectedSource || s.CandleInfo.Source == selectedSource)));
_selectedSecurities.AddRange(securities);
})
.ContinueWith(sec =>
{
...
}, System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext());
}
Недостаток здесь такой же - чтение данных напрямую из хранилища.
Видимо для того, чтобы при этом не тормозило само переключение вкладки, чтение вынесено в отдельный поток.
Ускорить переключение вкладок можно опять же за счет использование кэша:
Код
private void FillSecurities()
{
var selectedSource = Source.Name;
SecuritiesCtrl.Securities.Clear();
_selectedSecurities.Clear();
var securities = SecurityStorage.CachedSecurities
.Where(s => s.IsSelected())
.Select(s => s.ToVisualSecurity()).Where(s =>selectedSource == null || (s.TradeInfo.Source == selectedSource ||
s.DepthInfo.Source == selectedSource || s.OrderLogInfo.Source == selectedSource || s.SecurityChangeInfo.Source == selectedSource ||
s.Source == selectedSource || s.CandleInfo.Source == selectedSource));
_selectedSecurities.AddRange(securities);
...
}
Так как из кэша все берется быстро, да и чтобы не возиться с GUI-синхронизацией, я разобрал использование отдельного потока.
5. Последний нюанс, который вызвал у меня сомнения: зачем при старте импорта маркет-данных обновляются инструменты?
Необходимость этого я себе объяснить не смог, поэтому поменял этот код в классе MarketDataTrader:
Код
public void Start()
{
Trader = _createTrader();
try
{
...
Trader.SecuritiesChanged += OnSecuritiesChanged;
using (var su = new SecurityUpdate(Trader))
{
Trader.Connect();
lock (_connectedLock)
{
if (!Trader.IsConnected && !Monitor.Wait(_connectedLock, TimeSpan.FromSeconds(20)))
throw new TimeoutException("Ожидание подключения превысило максимально допустимый интервал.");
}
su.Wait();
}
}
catch
{
...
}
}
На следующий:
Код
public void Start(bool updateSecurities = false)
{
Trader = _createTrader();
try
{
...
Trader.SecuritiesChanged += OnSecuritiesChanged;
if (updateSecurities)
{
using (var su = new SecurityUpdate(Trader))
{
Trader.Connect();
lock (_connectedLock)
{
if (!Trader.IsConnected && !Monitor.Wait(_connectedLock, TimeSpan.FromSeconds(20)))
throw new TimeoutException("Ожидание подключения превысило максимально допустимый интервал.");
}
su.Wait();
}
}
else
{
Trader.Connect();
lock (_connectedLock)
{
if (!Trader.IsConnected && !Monitor.Wait(_connectedLock, TimeSpan.FromSeconds(20)))
throw new TimeoutException("Ожидание подключения превысило максимально допустимый интервал.");
}
}
}
catch
{
...
}
}
При этом причем updateSecurities == true только при запуске из метода MarketDataTrader.GetNewSecurities.
Вот такие оптимизации я сделал в своей Гидре версии 4.1.3.
Предлагаю разработчикам StockSharp высказать свое мнение по поводу предложенных изменений и внести удачные в Гидру.