О плохом и хорошем коде для чайников. Часть 2

23 19

Предисловие

Нет плохих и хороших языков программирования,

есть неразумное использование их возможностей.

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

Дьявол, как известно, кроется в деталях: если раньше мы говорили о культуре кода в целом, то теперь настало время поговорить о хорошем коде на ISBL. Как и любой другой язык программирования, он имеет свои особенности и ограничения, о которых и пойдёт речь.

ISBL

А что это за девочка, и где она живёт?

Какая-то не очень новая песня

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

ISBL, IS-Builder Language – встроенный в IS-Builder высокоуровневый язык программирования, предназначенный для описания алгоритмов работы прикладных задач.

На языке ISBL задаются все программные вычисления внутри систем, разработанных на платформе IS-Builder:

  • обработка событий при работе со справочниками и карточками документов;
  • подготовка данных для отчетов;
  • тексты сценариев и прикладных функций;
  • задание логики работы типовых маршрутов и их отдельных блоков;
  • вычисление ролей и описание поисков объектов в системе.

Объектная модель

Весь мир – GUI, и люди в нём – объекты.

Совершенно точно не Уильям Шекспир

ISBL вполне можно назвать объектно-ориентированным языком программирования в широком смысле этого слова, но вы не сможете написать новый класс или кардинально изменить возможности уже существующих (доступно только расширение возможностей путём добавления новых реквизитов/действий). Однако в пределах платформы IS-Builder его возможности достаточно широки и позволяют решить практически любую задачу при наличии у разработчика определённой фантазии.

Базовым классом для большинства объектов системы является, как несложно догадаться, IObject. В общем случае это одна запись набора данных (IDataSet) и одно или несколько представлений (IView) со своими свойствами и методами для работы. Представления содержат методы и свойства для визуальной работы с компонентами, наборы данных, очевидно, - для работы с самими данными.

Рассмотрение схемы наследования классов во всех подробностях занимает немало времени и вряд ли годится для этой статьи. Поэтому чтение справки по этому вопросу останется на совести читателей, а пока можно просто перечислить основные объекты, с которыми приходится иметь дело прикладному разработчику на ISBL. Это уже упомянутые IObject, IView и IDataSet и вдобавок к ним:

  • ITask – задача;
  • IJob – задание (дополнительно IControlJob и INotice – соответственно задание-контроль и уведомление);
  • IEDocument – документ;
  • IReference – справочник;
  • IUser – пользователь/группа пользователей;
  • IList, IStringList – список и список строк;
  • И многие, многие другие, о которых можно прочитать в справке по ссылке
    https://club.directum.ru/webhelp/directum/5.4/index.html?om_obekty_directum.htm.

Для получения объектов используются служебные объекты – фабрики, к которым можно обратиться при помощи предопределённых переменных: Tasks, Jobs, Notices, References, EDocuments, ServiceFactory (позволяет получать пользователей и не только) и др.

А что произойдёт с объектом после его получения?

Работа с объектами

- Что же мы с тобой сегодня напишем, ручечка?

Неизвестный графоман

Всё, что угодно! В пределах возможностей платформы, разумеется, но тем не менее. Общая схема такова: разработчик должен получить объект, сделать с ним что-то в рамках имеющейся задачи, сохранить при необходимости, закрыть при возможности и очистить переменные (восстановить состояние объектов для корректного освобождения памяти).

Встроенная сборка мусора в IS-Builder срабатывает только при завершении вычислений, и это значит, что если состояние объектов необходимо восстановить до этого момента, то это придётся сделать вручную:

  • для каждого вызова функции Open, AddSelect, AddWhere, AddOrderBy, AddJoin и т.п. вызвать Close, DelSelect, DelWhere, DelOrderBy, DelJoin;
  • при завершении работы с объектом присвоить ему значение nil;
  • для всех созданных объектов выполнить их освобождение путем присвоения nil.

Пример работы с объектом IReference:


  RefRecord = References.Assignments.GetObjectByID(AssignmentID) //Получили объект

  ... //Сделали что-то

  RefRecord.Save() //Сохранили

  RefRecord.CloseRecord() //Закрыли запись справочника

  RefRecord.Close() //Закрыли справочник

  RefRecord = nil //Очистили ссылку

Почему настолько важно восстановление состояния объектов и освобождение переменных? Как минимум потому, что выполнение любых программных вычислений требует аппаратных ресурсов. Открытый без фильтра (что, кстати, тоже неправильно, но об этом чуть позже) справочник на 200 тысяч записей занимает 600 МБ оперативной памяти. Если его не закрыть после использования, когда он станет уже не нужен, оставшиеся вычисления, во-первых, будут выполняться медленнее, чем могли бы, а во-вторых, могут затребовать себе для выполнения аналогичное количество памяти, и в совокупности потребление памяти процессом SBRte превысит максимально допустимый порог в 1Гб (в новых версиях – 4Гб, на workflow – настраиваемое количество), что приведёт к аварийному завершению процесса.

Помимо этого, состояние объектов важно восстанавливать в порядке, обратном порядку их создания. При последовательном получении справочника и его детального раздела нельзя освобождать объект-справочник раньше объекта-раздела:

  RefRecord = References.Assignments.GetObjectByID(AssignmentID)

  foreach DDSRow in RefRecord.DetailDataSet(1)

  ...

  endforeach

  RefRecord = nil 

Очистка переменной RefRecord, хранящей единственную ссылку на запись справочника, приведет к попытке платформы освободить и сам объект IReference. Однако цикл foreach имеет неочевидную особенность: его переменные по завершению цикла не очищаются, и тогда наличие ссылки на детальный набор данных в переменной цикла DDSRow помешает освобождению объекта, что в конечном итоге приведёт к возникновению ошибки ESBWrapperAlive. Такие ошибки возникают в результате некорректной работы с объектами и сами по себе не страшны, но могут приводить к другим, уже более серьёзным и критичным ошибкам в работе системы (например, Access Violation).

Работа с данными

640 килобайт оперативной памяти хватит всем!

Билл Гейтс (но это не точно)

Как уже было сказано ранее, большой массив данных требует большого количества аппаратных ресурсов для их обработки. Отсюда вывод - при работе с любым набором данных нужно постараться максимально ограничить его объём. Какими средствами это можно сделать:

· Для справочников – использование метода AddWhere перед открытием. Применение комбинации Filter + Filtered = TRUE после открытия никак не повлияет на потребление памяти уже открытым набором данных.

· Для SQL-запросов – ограничение выборки наложением дополнительных условий в предложении WHERE. Лайфхак: для перебора результатов запроса в цикле можно использовать ISBL-объект IQuery, который работает быстрее, чем функции SQL() и CSQL().

Пример из жизни – у клиента дала сбой настроенная интеграция DIRECTUM версии 5.1 и SAP по справочнику «Контрагенты». Как выяснилось, в сценарии интеграции справочник (тот самый, на 200 тысяч записей) открывался для загрузки данных без фильтра, и в один прекрасный момент стал занимать всю память, выделенную под процесс SBRte. Процесс завершался по превышению максимально допустимого объёма занимаемой памяти, интеграция не отрабатывала, и спустя некоторое время пользователи забили тревогу, обнаружив, что данные контрагентов стали неактуальны. Исправление ситуации заняло 1 час анализа и программирования и 3ч нагрузочного тестирования на реальных данных, после чего интеграция восстановилась и продолжает благополучно работать и по сей день.

Помимо объёма данных, на потребление памяти и, как следствие, на производительность могут влиять и сторонние вычисления. К примеру, при сохранении записи справочника выполняются три события: Сохранение Возможность, Сохранение До и Сохранение После. Чаще всего при программном внесении изменений в справочник необходимости в их отработке нет, поэтому их можно отключить:


  RefRecord = References.Assignments.GetObjectByID(AssignmentID) //получить запись

  RefRecord.Events.AddCheckPoint() //запомнить состояние событий

  RefRecord.Events.DisableAll() //отключить все события

  ... //сделать что-то с записью

  RefRecord.Save() //сохранить изменения

  RefRecord.CloseRecord() //закрыть запись

  RefRecord.Close() //закрыть справочник

  RefRecord.Events.ReleaseCheckPoint() //восстановить состояние событий

  RefRecord = nil //очистить переменные

Самые внимательные читатели, скорее всего, уже заметили, что в некоторых приведённых примерах кода используются методы Close() и CloseRecord(), но нет предшествующих им Open() и OpenRecord(). Почему? Потому что использование метода GetObjectByID практически эквивалентно коду:

  AssignmentsRef = References.Assignments.GetComponent()

  AssignmentsRef.AddWhere(“dbo.MBAnalit.Analit = “ & AssignmentID)

  AssignmentsRef.Open()

  AssignmentsRef.OpenRecord()

Как можно увидеть, при вызове метода GetObjectByID() отрабатывают методы Open() и OpenRecord(), но события, соответствующие этим методам (Набор данных. Открытие, Запись.Открытие и, в некоторых случаях, Форма-карточка. Показ) не отключаются. Поэтому в случаях, когда счёт времени выполнения идёт на секунды или миллисекунды, лучше использовать приведённый выше пример, добавив перед вызовом Open() отключение событий.

Вместо заключения

У всех участников процесса разработки ПО есть свои маленькие профессиональные радости. Ничто так не радует разработчика и заказчика, как работающая быстро (даже на компьютерах в бухгалтерии) и безошибочно (особенно с первого раза, хотя это впечатление и бывает ошибочным) программа.  Аналитик (он же консультант) придёт в буйный восторг от грамотно составленного ТЗ, которое не надо исправлять. Пользователь будет просто счастлив, если ему не придётся долго искать ответы на неминуемо возникающие у него вопросы. А тестировщику придётся по душе как отсутствие ошибок, так и их грамотная обработка. Но это уже совсем другая история, которую мы расскажем в следующий раз.

23
Авторизуйтесь, чтобы оценить материал.
6
Александр Чугунов

AssignmentsRef.AddWhere(“Analit = “ & AssignmentID) работать не будет=)

 ISBL-объект IQuery, который работает быстрее, чем функции SQL() и CSQL()

- это с какой версии так?

Не знаю, намеренно ли этого нет в статье и планируется ли это написать в следующей, но 

1) Если уж зашла речь про GetObjectByID() и производительность то, имхо, следовало бы написать, что не стоит везде использовать этот метод, только когда нам надо поменять запись, в большинстве случаев чтения значений делать OpenRecord() не надо. Лайкаем эту идею, ее реализация очень прилично ускорит работу ОМ.
Для получения записи без открытия и без отработки событий удобно использовать GetFilteredReferenceRecord().

2) По ограничению получаемого набора данных не упомянуто про Preloaded у реквизита и соответствующих параметрах функций CreateReference() и GetFilteredReferenceRecord(). По опыту выборка только нужных реквизитов очень существенно ускоряет получение данных с SQL.

Александр, не намеренно =)

Вы правы, GetObjectbyID действительно в большинстве случаев будет избыточен. О свойстве Preloaded у реквизитов и функциях CreateReference и GetFilteredReferenceRecord в статье не сказано тоже не по злому умыслу, просто в разработке эти варианты встречаются на порядок реже, чем описанные.

Использование IQuery в цикле foreach работает быстрее, чем CSQL или CSubString по отношению к результату SQL, не критично, но быстрее. С какой версии, точно не скажу, последний раз проверяла на 5.2.1. Думаю, тут могут более опытные коллеги прокомментировать.

Пример с AddWhere поправила, спасибо =)

Александр Чугунов

Александра, я почему спросил про скорость IQuery и SQL(): вспомнилась вот эта статья и то, что там преимущество IQuery по скорости называют мифом (я считаю что лучше пользоваться IQuery в любом случае, тем более что в последних версиях он параметризуем в отличие от SQL() и CSQL()).
По Preloaded наверно правильно в статье не написали, думаю что это уже оптимизация следующего уровня, по сравнению с использованием AddWhere вместо Filter указание загружаемых реквизитов дает малый прирост.
 

Александр, автор той статьи не так уж и не прав, в моём случае использование IQuery вместо SQL дало прирост производительности всего на 3%. Это вполне объяснимо, потому что для более ощутимого ускорения работы нужно менять сам запрос, а не средства его выполнения =)

Александр Чугунов

Александра, тут дело не в запросе, а в способе перебора результатов. Запрос должен одинаково выполняться и там и там, а вот перебор результатов - его скорость зависит от ISBL и от того, работаем через ОМ или функции. В общем лайфхак - он в целом правильный, вот только причина, почему стоит использовать IQuery - отнюдь не скорость выполнения.

Александр, и вы снова правы. Я думаю, что тогда это уже не столько лайфхак, сколько пример best practices, а best practices заслуживают отдельной статьи.

Андрей Дозоров

На самом деле IQuery сейчас явно быстрее работает (версия 5.2.1)...
Простенький код
 

  Query = 'select top 10000 NameAn as NameAn, Vid as Vid, Analit as Analit from mbanalit'
  QueryData = CreateQuery()
  QueryData.CommandText = Query
  StartTimeTest1 = ServerDateTime()
  QueryData.Open
  foreach Data in QueryData
    Vid = Data.Fields('Vid').AsString
  endforeach
  StopTimeTest1 = ServerDateTime()
  Data = NULL
  QueryData = NULL
  
  StartTimeTest2 = ServerDateTime()
  QueryData = SQL(Query)
  foreach data in CSubString(QueryData; '|')
    Vid = SubString(data; ';'; 2)
  endforeach
  StopTimeTest2 = ServerDateTime()
  
  EditText(StartTimeTest1 & CR & StopTimeTest1 & Cr & CR & StartTimeTest2 & Cr & StopTimeTest2)    

дал результат
25.07.2017 17:46:14
25.07.2017 17:46:15

25.07.2017 17:46:15
25.07.2017 17:46:25
Думаю приличная разница... Скорее всего за последние 4 года в платформе пошаманили с IQuery.

По поводу GetObjectbyID, тут кроме открытия записи есть еще одно событие, которое может сильно усугубить время выполнения - набор данных открытие, это событие далеко не всегда нужно и его можно отключать.
По поводу циклов - если в цикле будет очень много итераций, желательно бы минимизировать использование прикладных функций в цикле (именно прикладных), почему - если я не ошибаюсь, на каждый вызов прикладной функции билдер создает отдельный экземпляр интерпретатора, что само собой требует чуть больше времени, чем если бы код был написан в самом цикле без обертки в функцию. Для пары итераций - не критично, а вот когда счет идет на тысячи, может быть чувствительно.

Александр Чугунов

Андрей, а сколько раз Вы Ваш тест запускали? У меня время одно и то же получается = 1 сек (5.0). Запускаю, правда, на машине с SQL-сервером.
Надо бы разобраться в чем дело, какие тут тонкости могут быть...

Андрей Дозоров

Александр, запускал 5 раз сегодня утром, столько же вчера, результат всегда примерно одинаковый. Запускал на машине (intel xeon 2.13GHz 4ядра, 16 Гб ОП, 64битная ОС), система 5.2.1.
Попробовал на своей рабочей ЭВМ запустить, результаты уже немного другие, 1 сек против 5 сек, машина intel i3 3.07GHz, 64битная ОС (системы 5.1 и 5.2).
В качестве наблюдения - при работе сценария больше всего потребляется ЦПУ клиентской машины (у меня до 40%), видимо работа SubString сильно зависит от процессора.

Александр Чугунов

Андрей, я попробовал еще на одной машине, результат одинаковый для разных способов. Вот где я пробовал (5.0)
1) Мой старый ноут: Core i5 M520 2.4ГГц 2 ядра, 6 ГБ, 64-битная WIN10
2) Сервер (виртуалка на HyperV): core i7-4770 3.4ГГц, 6 ГБ, 32-битная 2008R2
Причем на моем ноуте на ~10% быстрее, чем на виртуалке=) Пробовал top 10К, 100К, 400К - для разных способов не различается, время растет примерно линейно.

Андрей Дозоров

Александр, тоже проверил на 5.0, результаты получаются такие же как у вас, IQuery и SubString дают примерно одинаковое время...
Получается начиная с 5.1 SubString начал медленнее работать...

Александр Чугунов

Андрей, думаете именно SubString? Может в целом вызов всех системных функций стал медленнее работать. Причем медленнее почти в 10 раз... Напишите платформщикам чтоль=)

Валерий Самарин

CSQL такая же как iQuery (судя по результатам) :)
 

Query = 'select top 10000 NameAn as NameAn, Vid as Vid, Analit as Analit from dbo.MBAnalit'
  QueryData = CreateQuery()
  QueryData.CommandText = Query
  StartTimeTest1 = ServerDateTime()
  QueryData.Open
  foreach Data in QueryData
    Vid = Data.Fields('Vid').AsString
  endforeach
  StopTimeTest1 = ServerDateTime()
  Data = NULL
  QueryData = NULL
  
  StartTimeTest2 = ServerDateTime()
  QueryData = SQL(Query)
  foreach data in CSubString(QueryData; '|')
    Vid = SubString(data; ';'; 2)
  endforeach
  StopTimeTest2 = ServerDateTime()
  
  StartTimeTest3 = ServerDateTime()
//  QueryData = SQL(Query)
  foreach data in CSQL(Query;; ';')
    Vid = SubString(data; ';'; 2)
  endforeach
  StopTimeTest3 = ServerDateTime()

  EditText(StartTimeTest1 & CR & StopTimeTest1 & Cr & CR & StartTimeTest2 & Cr & StopTimeTest2 & Cr & CR & StartTimeTest3 & Cr & StopTimeTest3)    

Результаты:
26.07.2017 13:13:00
26.07.2017 13:13:01

26.07.2017 13:13:01
26.07.2017 13:13:06

26.07.2017 13:13:06
26.07.2017 13:13:06

Версия 5.4.1

Александр Чугунов

Посмотрите кто-нибудь что конкретно медленно работает: SQL() или foreach data in CSubString(QueryData; '|'). Желательно для разного количества строк.

Андрей Дозоров

Сам по себе SQL работает быстро, все время уходит на работу CSubString.

Евгений Стоянов

Вопрос для знатоков, как правильно с точки зрения освобождения памяти через Api (c#)?
так:

SBRte.IDialog dlg = app.DialogsFactory.DialogFactory["Диалог"].CreateNew();         

 dlg.Requisites["Реквизит"].Value = app.ReferencesFactory.ReferenceFactory["Справочник"].GetObjectByID(ИД).Requisites["Код"].Value

Marshal.FinalReleaseComObject(dlg);

Marshal.FinalReleaseComObject(app);

или так:

SBRte.IDialog dlg = app.DialogsFactory.DialogFactory["Диалог"].CreateNew();         

ref = app.ReferencesFactory.ReferenceFactory["Справочник"].GetObjectByID(ИД);

 dlg.Requisites["Реквизит"].Value = ref.Requisites["Код"].Value

ref.Closerecord();

ref.Close();

Marshal.FinalReleaseComObject(dlg);

Marshal.FinalReleaseComObject(ref);

Marshal.FinalReleaseComObject(app);

Евгений Стоянов: обновлено 08.08.2017 в 09:06
Евгений Стоянов: обновлено 08.08.2017 в 09:06
Евгений Стоянов: обновлено 08.08.2017 в 09:07
Александр Чугунов

Евгений, правильнее не получать запись по ИД только для того, чтобы получить Код =) Делайте это SQL-запросом. На худой конец получайте инфошку с помощью ObjectInfo.

Александр Чугунов

Евгений, а еще недавно был почти такой же вопрос, на который Денис Мурашов очень хорошо ответил: Высвобождение памяти SBRte.exe при работе через COM из C#

Евгений Стоянов

Спасибо! =)

Авторизуйтесь, чтобы написать комментарий