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

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


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

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

Теги:


Спасибо: Mikhail Sukhov StockSharp


Цифровой

Фотография
Дата: 07.10.2012
Ответить


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

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

Mikhail Sukhov

Фотография
Дата: 08.10.2012
Ответить


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

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


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

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

Moadip

Фотография
Дата: 08.10.2012
Ответить


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


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

Цифровой

Фотография
Дата: 08.10.2012
Ответить


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

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

Mikhail Sukhov

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


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

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

Mikhail Sukhov

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

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

Цифровой

Фотография
Дата: 08.10.2012
Ответить


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


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


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

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


Добавить файлы через драг-н-дроп, , или вставить из буфера обмена.

loading
clippy