При разработке сложных аналитических рабочих мест и обработок в среде управляемого интерфейса 1С разработчики нередко сталкиваются с ситуацией, когда вывод разветвленного дерева значений приводит к ощутимым задержкам интерфейса. Пользователь нажимает клавишу стрелки вниз или кликает мышью по узлу дерева, и платформа замирает на 3–4 секунды. Аналогичные зависания могут проявляться и при обычном переключении между закладками формы. Разберем по шагам, из-за чего возникает эта проблема и как оптимизировать работу с большими иерархическими структурами.
Для обнаружения узкого места в первую очередь запустим замер производительности в конфигураторе. На практике почти всегда выясняется, что само дерево отрисовывается быстро, а задержки возникают в обработчике события ПриАктивизацииСтроки. Проанализируем архитектурные факторы, которые приводят к деградации скорости:
ДанныеФормыДерево. Если при каждой смене строки в обработчике вызывается серверная процедура с директивой &НаСервере, платформа выполняет контекстный вызов. В этот момент весь объем данных формы (тысячи строк с множеством колонок) сериализуется и передается на сервер, а затем обратно. Даже в толстом клиенте обычного приложения внутренняя сериализация контекста вызывает секундные лаги.ДанныеФормыКоллекция. У них отсутствует объект Индексы, свойственный серверной ТаблицаЗначений. Использование ручных переборов или метода НайтиСтроки по большим связанным таблицам на клиенте выполняет последовательное сканирование строк со сложностью O(N).Рассмотрим первый шаг оптимизации: предотвратим повторные и ложные запуски тяжелого алгоритма. Заведем в модуле формы клиентскую переменную, в которой будем сохранять идентификатор текущей активной строки. Если событие вызвано повторно (например, при переключении между вкладками формы или потере фокуса), выполнение алгоритма прерывается мгновенно.
Посмотрим на пример реализации в модуле формы:
&НаКлиенте
Переменная ПредыдущаяСтрокаДерева;
&НаКлиенте
Процедура ДеревоПриАктивизацииСтроки(Элемент)
ТекущийИдентификатор = Элемент.ТекущаяСтрока;
Если ТекущийИдентификатор = Неопределено
Или ТекущийИдентификатор = ПредыдущаяСтрокаДерева Тогда
Возврат;
КонецЕсли;
ПредыдущаяСтрокаДерева = ТекущийИдентификатор;
// Вызов логики обновления подчиненных данных
ОбновитьПодчиненныеДанные(ТекущийИдентификатор);
КонецПроцедуры
Часто в обработчике ПриАктивизацииСтроки пытаются вручную отфильтровать соседнюю таблицу через обход строк или метод НайтиСтроки. Однако платформа предоставляет встроенный интерфейсный механизм клиентской фильтрации через свойство элемента управления таблицы ОтборСтрок.
Разберем пример, в котором подчиненная таблица привязывается к ключу выбранного узла дерева без единого серверного вызова и без линейного перебора коллекции в цикле:
&НаКлиенте
Процедура ОбновитьПодчиненныеДанные(ИдентификаторСтроки)
ТекущиеДанные = Элементы.ДеревоДанных.ТекущиеДанные;
Если ТекущиеДанные = Неопределено Тогда
Возврат;
КонецЕсли;
// Устанавливаем быстрый визуальный отбор в подчиненной таблице
КлючСвязи = ТекущиеДанные.ИдентификаторУзла;
Элементы.ПодчиненнаяТаблица.ОтборСтрок = Новый ФиксированнаяСтруктура("КлючСвязи", КлючСвязи);
КонецПроцедуры
Если для получения подчиненных данных серверный запрос все же строго необходим, категорически исключим контекстные процедуры &НаСервере. Объявим метод с директивой &НаСервереБезКонтекста и передадим в него только минимально необходимые параметры (ссылку на элемент, идентификатор узла), исключив передачу структуры всей формы.
Посмотрим на корректную схему вызова:
&НаСервереБезКонтекста
Функция ПолучитьСвязанныеДанныеНаСервере(Знач СсылкаНаУзел)
// Выполняем точечный запрос только для текущей строки
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура,
| Т.Количество
|ИЗ
| РегистрСведений.СпецификацииНоменклатуры КАК Т
|ГДЕ
| Т.Владелец = &СсылкаНаУзел";
Запрос.УстановитьПараметр("СсылкаНаУзел", СсылкаНаУзел);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
При таком подходе платформа не тратит процессорное время на упаковку всего дерева значений в 1100 узлов, а передает через клиент-серверную границу только одну ссылку и компактный результат запроса.
Когда пользователь быстро листает дерево стрелками на клавиатуре, событие ПриАктивизацииСтроки вызывается для каждой промежуточной позиции. В результате система ставит в очередь десятки ресурсоемких операций. Для решения этой проблемы применим паттерн отложенного выполнения: сбросим и перезапустим короткий таймер через метод ПодключитьОбработчикОжидания.
Рассмотрим, как это реализовать в клиентском коде:
&НаКлиенте
Процедура ДеревоПриАктивизацииСтроки(Элемент)
// Отключаем предыдущий запланированный вызов, если курсор двигается дальше
ОтключитьОбработчикОжидания("ВыполнитьОтложенноеОбновление");
// Запускаем однократное обновление с паузой 0.3 секунды
ПодключитьОбработчикОжидания("ВыполнитьОтложенноеОбновление", 0.3, Истина);
КонецПроцедуры
&НаКлиенте
Процедура ВыполнитьОтложенноеОбновление()
// Этот код сработает только тогда, когда пользователь остановился на строке
ТекущиеДанные = Элементы.ДеревоДанных.ТекущиеДанные;
Если ТекущиеДанные <> Неопределено Тогда
ОбновитьСвязаннуюИнформацию(ТекущиеДанные.Ссылка);
КонецЕсли;
КонецПроцедуры
Если вся информация для детализации может быть подготовлена при первичном открытии формы или нажатии кнопки заполнения, исключим постоянные поиски в коллекциях формы. Заполним на сервере структуру данных и передадим ее на клиент в объект Соответствие, где ключом будет выступать идентификатор строки или ссылка, а значением — массив или структура подчиненных строк.
Так как доступ по ключу в объекте Соответствие осуществляется за константное время O(1), мгновенное получение данных из кеша полностью устраняет зависание интерфейса даже при работе с масштабными иерархиями.