Ускорение работы Гидры при большом количестве инструментов


Ускорение работы Гидры при большом количестве инструментов
Atom
10/7/2012


Доброго времени суток!

Недавно обнаружил, что Гидра начинает прилично подтормаживать при большом количестве инструментов.
Получил такую ситуацию довольно просто: запустил импорт инструментов у источника Smart (из демо-сервера),
в ходе которого мне прилетело около 30 000 инструментов.
После этого запуск Гидры и переход между вкладками начали тормозить,
а запуска импорта маркет-данных из Smart было не дождаться: он отваливался по таймауту.

Начал копать и обнаружил, что долго выполняется чтение данных из хранилища:

Code

entityRegistry.Securities


Сразу оговорюсь: в качестве хранилища я использую SQLLite, и, возможно, MS SQL Server тормозит значительно меньше.


0. Понятно, что хотя бы один раз данные об инструментах из базы данных зачитать надо.
Это происходит в классе SecurityStorage:

Code

public SecurityStorage(IEntityRegistry entityRegistry)
{
	if (entityRegistry == null)
		throw new ArgumentNullException("entityRegistry");
	foreach (var security in entityRegistry.Securities)
		AddToCache(security);
}

Здесь все зачитанные данные мудро кладутся в кэш.


1. Но вот если посмотреть на класс FinamSecurityStorage, то здесь все уже не так радужно:

Code

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,
но можно сделать вот такой "костыль":

Code

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:

Code

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 подряд чтений из хранилища, что и приводит к превышению довольно солидного тайм-аута при старте.

Оптимизировать выполнение можно, построив один раз массив и использовав его для группировки:

Code

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 аналогичный):

Code

var selectedSecurities = _entityRegistry.Securities.Select(s => s.ToVisualSecurity()).Where(s => s.IsSelected);

if (_worker.Start(selectedSecurities))
{
	...
}
else
{
	...
}


Здесь тоже можно воспользоваться имеющимся кэшем и переписать построение выбранных интрументов таким образом:

Code

var selectedSecurities = _securityStorage.CachedSecurities
	.Where(s => s.IsSelected())
	.Select(s => s.ToVisualSecurity())
	.ToArray();

В принципе, если бы код был написан сразу так, то оптимизация номер 2 с GroupBy даже не нужна.
Но, имхо, правильнее сделать и то, и другое.


4. Еще невыносимо долго при большом числе инструментов переключаются вкладки.
Код построения всех выбранных интрументов в классе MarketDataSourceControl такой:

Code

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());
}

Недостаток здесь такой же - чтение данных напрямую из хранилища.
Видимо для того, чтобы при этом не тормозило само переключение вкладки, чтение вынесено в отдельный поток.

Ускорить переключение вкладок можно опять же за счет использование кэша:

Code

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:

Code

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
	{
		...
	}
}


На следующий:

Code

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 высказать свое мнение по поводу предложенных изменений и внести удачные в Гидру.

Tags:


Thanks: Mikhail Sukhov StockSharp


Цифровой

Avatar
Date: 10/7/2012
Reply


С другой стороны, даже не смотря на все эти оптимизации, сам запуск Гидры будет долгим,
т.к. хотя бы один раз инструменты зачитать придется.
Поэтому если по факту используется небольшое количество инструментов, разумно остальные просто удалить.

У себя я даже реализовал такую команду в Гидре.
Но работала она довольно долго. Удаление 1000 инструментов из SQLLite занимает примерно минуту.
Поэтому удаление моих 30 000 заняло около получаса.
Единственный плюс по сравнению со сносом базы: не пришлось забивать настройки ручками заново.
Thanks:

Mikhail Sukhov

Avatar
Date: 10/8/2012
Reply


Хорошее исследование.[thumbup]

Цифровой Go to
Удаление 1000 инструментов из SQLLite занимает примерно минуту.


Надо удалять в транзакции, возрастает скорость на порядки. Известная особенность SQLite.

Насчет инструментов, то тут не все однозначно. С одной стороны грузить можно только то, что нужно. Но некоторым источникам (например как РТС) нужно в моменте достаточно большое кол-во инструментов. Финаму же - нет. С другой стороны, может оказаться так, что быстрее при старте загрузить всю информацию. И это время компенсирует задержки, вызванные отложенными загрузками во время "работы" Гидры. Которые мы, кстати, можем и не заметить за счет того, что идет все в фоновом режиме. Но задержки эти будут.
Thanks:

Moadip

Avatar
Date: 10/8/2012
Reply


Quote:
Вот такие оптимизации я сделал в своей Гидре версии 4.1.3.


Посмотрите гидру последней версии 4.1.5. Там многое поменялось.
Thanks:

Цифровой

Avatar
Date: 10/8/2012
Reply


Mikhail Sukhov Go to
Хорошее исследование.[thumbup]

Спасибо! Надеюсь, что какие-то из оптимизаций появятся, если еще не появились, в будущих версиях Гидры.

Mikhail Sukhov Go to

Цифровой Go to
Удаление 1000 инструментов из SQLLite занимает примерно минуту.


Надо удалять в транзакции, возрастает скорость на порядки. Известная особенность SQLite.

Спасибо за совет. Попробую. Когда получится предложу код.

Mikhail Sukhov Go to

Насчет инструментов, то тут не все однозначно. С одной стороны грузить можно только то, что нужно. Но некоторым источникам (например как РТС) нужно в моменте достаточно большое кол-во инструментов. Финаму же - нет. С другой стороны, может оказаться так, что быстрее при старте загрузить всю информацию. И это время компенсирует задержки, вызванные отложенными загрузками во время "работы" Гидры. Которые мы, кстати, можем и не заметить за счет того, что идет все в фоновом режиме. Но задержки эти будут.

Согласен. Именно по этому я прежде всего пытался оптимизировать работу, а уже потом написал команду удаления "невыбранных" инструментов.
Thanks:

Цифровой

Avatar
Date: 10/8/2012
Reply


Moadip Go to
Quote:
Вот такие оптимизации я сделал в своей Гидре версии 4.1.3.


Посмотрите гидру последней версии 4.1.5. Там многое поменялось.


Верное замечание.
Перед публикацией поста я посмотрел, что вышли новые версии.
Правда на BOX версия 4.1.5 без исходников (на CodePlex посмотреть как-то не догадался).
В версии 4.1.4 хотя бы часть оптимизаций не реализована (все не просматривал)

Если уже в версии 4.1.5 что-то из перечисленного мной поправили - это супер!
Thanks:


Attach files by dragging & dropping, , or pasting from the clipboard.

loading
clippy