Утечка памяти в обработчике SetTableNotificationCallback

Страницы: 1
RSS
Утечка памяти в обработчике SetTableNotificationCallback, Функция обратного вызова обработчика событий пользовательской таблицы не освобождает память между вызовами
 
Добрый день.

Буду признателен за помощь!

На QLUA создаю пользовательскую табличку, на которую при помощи функции SetTableNotificationCallback вешаю обработчик нажатий клавиш. По нажатию ПРОБЕЛ обработчик должен запросить некоторые данные из таблиц QUIK и отобразить их в таблице. Обработчик использует только локальные переменные, которые, по идее, должны быть освобождены после выхода из обработчика, однако, если в окне (Меню Сервисы -> Lua скрипты... -> Окно Доступные скрипты) пронаблюдать динамику объёма памяти, который скрипт занимает, то видно, что при каждом нажатии пробела (т.е. при каждом вызове обработчика) объем занятой памяти только равномерно увеличивается, но никогда не освобождается. Исходник ниже, достаточно его просто Copy/Paste к себе, чтобы воспроизвести проблему.


Код
-- ************************************************
-- *    Обработчик событий для таблицы            *
-- ************************************************

function UpdateTableData(t_id, msg, par1, par2)

   if msg == QTABLE_VKEY and par2 == 32 then -- нажата клавиша "Пробел", 

      local records = {}
      local i = 0

      local option_class_code = "SPBOPT"
      local ba_sec_code = "RIM4"
      local sec_list = getClassSecurities(option_class_code)
      for option_sec_code in string.gmatch(sec_list, "([%.%w]+)") do
         local securityInfo = getSecurityInfo(option_class_code, option_sec_code)
         if securityInfo ~= nil 
         and securityInfo.base_active_seccode == ba_sec_code 
         and 20240501 < securityInfo.exp_date
         then
            local sec_type = getParamEx(option_class_code, option_sec_code, "OPTIONTYPE").param_image
            if sec_type == "Put" then 
               i = i + 1
               local type_execution = getParamEx(option_class_code, option_sec_code, "OPTIONEXECTYPE").param_image
               records[i] = {
                  ba_sec_code = ba_sec_code,
                  option_sec_code = option_sec_code,
                  option_sec_code_full = securityInfo.name,
                  option_class_code = option_class_code,
                  exp_date = securityInfo.exp_date,
                  ba_class = "SPBFUT", 
                  vid_option = "Американский", 
                  type_execution = type_execution,
                  option_strike = securityInfo.option_strike,
                  format = "%." .. securityInfo.scale .. "f",
               }
            end -- sec_type
         end -- if securityInfo
      end -- for option_sec_code

      -- -- Now draw the table
      Clear(Tt_id) 
      for k,v in pairs(records) do
         InsertRow(Tt_id, k)
         SetCell(Tt_id, k, 1, v.ba_sec_code)
         SetCell(Tt_id, k, 2, v.ba_class)
         SetCell(Tt_id, k, 3, v.option_sec_code_full)
         SetCell(Tt_id, k, 4, v.option_sec_code)
         SetCell(Tt_id, k, 5, v.option_class_code)
         SetCell(Tt_id, k, 6, string.sub(v.exp_date , 7,8) .. "." .. string.sub(v.exp_date , 5, 6) .. "." .. string.sub(v.exp_date , 1, 4))
         SetCell(Tt_id, k, 7, v.vid_option)
         SetCell(Tt_id, k, 10, v.type_execution)
         
      end -- for k,v 
      SetSelectedRow(Tt_id, 1) -- установить курсор на запись-чемпиона
   end -- if  msg == QTABLE_CHAR 
end -- UpdateTableData



-- *********************************************
-- *          Обарботчик запуска скрипта       *
-- *********************************************

function OnInit(script_path)

   Tt_id = AllocTable()
   AddColumn(Tt_id, 1, "Б.А.", true, QTABLE_STRING_TYPE, 10)
   AddColumn(Tt_id, 2, "Б.А.Класс", true, QTABLE_STRING_TYPE, 15)
   AddColumn(Tt_id, 3, "Код инструмента (полн.)", true, QTABLE_STRING_TYPE, 25)
   AddColumn(Tt_id, 4, "Код инструмента", true, QTABLE_STRING_TYPE, 15)
   AddColumn(Tt_id, 5, "Код Класса", true, QTABLE_STRING_TYPE, 10)
   AddColumn(Tt_id, 6, "Погашение", true, QTABLE_DATE_TYPE, 10)
   AddColumn(Tt_id, 7, "Вид опциона", true, QTABLE_STRING_TYPE, 15) -- Американский / Европейский
   AddColumn(Tt_id, 8, "Тип опциона", true, QTABLE_STRING_TYPE, 5) -- Call / Put
   AddColumn(Tt_id, 9, "Тип расчёта", true, QTABLE_STRING_TYPE, 15) -- Маржа / Премия
   AddColumn(Tt_id, 10, "Тип исполнения", true, QTABLE_STRING_TYPE, 12) -- Поставка / Расчёт
   AddColumn(Tt_id, 11, "Страйк", true, QTABLE_DOUBLE_TYPE, 12) 
   AddColumn(Tt_id, 12, "Цель", true, QTABLE_DOUBLE_TYPE, 12) 
   AddColumn(Tt_id, 13, "Профит, пт", true, QTABLE_INT_TYPE , 12) 
   AddColumn(Tt_id, 14, "Теор. цена", true, QTABLE_DOUBLE_TYPE, 12) 
   AddColumn(Tt_id, 15, "Доходность", true, QTABLE_DOUBLE_TYPE, 8) 

   CreateWindow(Tt_id)

   SetTableNotificationCallback(Tt_id, UpdateTableData) 

   SetWindowCaption(Tt_id, "test 1.0")
   SetWindowPos(Tt_id, 1, 259, 1500, 655)
   InsertRow(Tt_id, -1) -- Зачем-то просят вставить эту запсиь ...
   
end -- OnInit()



-- *********************************************
-- *          Обарботчик останова скрипта       *
-- *********************************************
function OnStop()
   DestroyTable(Tt_id)
   Tt_id = nil
   IsRun = false
end

-- *********************************************
-- *          Start running program            *
-- *********************************************
IsRun = true

function main()
   while IsRun do sleep(1000) end
end

 

На скрине показан объём занятой памяти в моменте. Для воспроизведения проблемы нужно несколько раз нажать ПРОБЕЛ при нахождении фокуса ввода на таблице и пронаблюдать обведённое красным кружком значение.

Оно увеличивается.

Ожидаемое поведение: последовательное нажатие ПРОБЕЛ и вызов обработчика не приводят к постоянному росту объёма занимаемой памяти.


Буду рад Вашим соображениям как можно решить эту проблему.

С уважением,

Роман.

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

Для начала, это лучше не делать в колбеке, а передавать реакцию в поток скрипта.
Также лучше получить весь список инструментов один раз, при старте скрипта. А при обработке реакции идти по уже полученному списку и обновлять данные только для него.
 
Nikolay, благодарю Вас за внимание к моему вопросу.

Я понимаю, что запрашиваю значительный объём данных, но ведь он весь сохраняется в локальных переменных колбека, которые по выходу из него должны быть освобождены и вернуты в "кучу", однако почему-то этого не происходит. Автор Lua в своей книге пишет "Finally, a local variable vanishes as soon as its scope ends, allowing the garbage collector to release its value." Так что утечка остаётся загадкой...

Это не первый скрипт у меня, где колбэк на таблице запрашивает данные и что-то вычисляет и в других сценариях такой проблемы нет.

Я не совсем понял Вашу фразу "это лучше не делать в колбеке, а передавать реакцию в поток скрипта". Вы имеете ввиду основную обработку делать в функции main() ? А что и как мне тогда туда передавать ? Не могли бы уточнить эту мысль?

При старте скрипта один раз весь список инструментов тащить не совсем подходит - нужны конкретные инструменты, коды которых скрипт подбирает из файла (я тут убрал эту логику, потому что она не имеет отношения к проблеме). Просто хотел конкретно понять почему происходит memory leak при том, что все переменные локальные и должны бы освобождаться...
 
Роман, если Вы вручную не зовёте сборщик мусора, он в своей манере вызывается. Может быть, ещё не пришло время по его настройкам. Попробуйте вручную вызвать. Если рост останется, тогда надо будет кумекать.
 
Цитата
Роман написал:
Я не совсем понял Вашу фразу "это лучше не делать в колбеке, а передавать реакцию в поток скрипта". Вы имеете ввиду основную обработку делать в функции main() ? А что и как мне тогда туда передавать ? Не могли бы уточнить эту мысль?
Сборщик мусора не вызывается на каждый тик. Поэтому какое-то время они там останутся.

Передать очень просто. Может прочитать примеры из руководства "Использование Lua в Рабочем месте QUIK". Хотя все очевидно - в колбеке какой-то переменной, записи в очереди, присваиваете какое событие произошло, а в main обрабатываете.

Цитата
Роман написал:
При старте скрипта один раз весь список инструментов тащить не совсем подходит - нужны конкретные инструменты, коды которых скрипт подбирает из файла
Тем более лучше тогда это делать не в колбеке. При старте читаете файл, проверяете наличие инструмента и создаете необходимый список. А уже при обработке этот готовый список используете.
Ваш подход предполагает, что этот список изменяется и его надо постоянно обновлять. Но если это не так, то зачем каждый раз с сервера получать одни и те же данные.
 
Nikolay, funduk,

Благодарю вас за ваши советы - они помогли решить проблему!


Nikolay, я переделал скрипт как Вы порекомендовали - сделал обработчик, который только выставляет в глобальной переменной флаг, что была нажата клавиша ПРОБЕЛ:

Код
function TableHandler(t_id, msg, par1, par2)
   if msg == QTABLE_VKEY and par2 == 32 then bUpdate = true end   
end

а в функции main() уже тестирую этот флаг и если он true - соответственно вызываю функцию, которая делает все вычисления:


Код
function main()

   while IsRun do
      
      sleep(Sleep_period)

      if bUpdate then UpdateTableData() end

      bUpdate = false

   end -- while

end -- function main()


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

funduk, тоже думал уже про явные вызовы сборщика мусора, но как это не совсем идеологически верно, мне кажется. К счастью, вынос основных вычислений в поток скрипта (main()) благополучно решил проблему. Теперь всё работает так как и ожидалось.

Ещё раз благодарю вас, коллеги, за внимание к вопросу и за помощь!

Хорошего всем дня  :cool: !
 
У меня в связи с выносом обработки в main вопрос: напр., я хочу вынести из OnQuote разбор стакана в main, OnQuote будет вызывать getQuoteLevel2 и запоминать результат в циклический массив, а main будет брать его оттуда по текущему индексу, обрабатывать и освобождать память под эту табличку, которую получила через OnQuote от getQuoteLevel2. Не хочется использовать медленные sinsert и sremove. Можно ли сделать такую схему, чтобы не было конфликтов в связи с параллельным использованием данных между потоком main и потоком коллбэков?
 
Цитата
Serge123 написал:
OnQuote будет вызывать getQuoteLevel2 и запоминать результат в циклический массив
Это тоже лучше делать в main. Число реакций на изменение стакана может быть десятки раз в секунду. Если не стоит задача сохранить состояние стакана в каждый момент времени, то лучше разбирать стакан по событиям, когда поток скрипта дойдет до этого.

Для примера, скрипт из сотни тысяч строк и его один цикл занимает до 1 секунды. Реагировать на изменения стакана, когда скрипт занят другим не имеет смысла, т.к. когда скрипт дойдет до разбора, его состояние изменится десятки раз. И все эти промежуточные состояния уже устарели. Но опять, если не стоит задача сохранить состояние стакана в каждый момент времени.
 
Цитата
Nikolay написал:
Это тоже лучше делать в main.
Мне желательно знать, когда было изменение в стакане, напр., чтобы знать, где я стою в очереди на сделки. Мне кажется, getQuoteLevel2 работает быстро, т.к. эту информацию даёт Квик, который уже получил её с сервера брокера.
К сожалению, квиковцы так и не сделали, чтобы можно было получать через OnQuote или хотя бы getQuoteLevel2 время с сервера биржи, в которое произошло изменение в стакане.

А что же с моим вопросом в #7?
 
Цитата
Serge123 написал:
У меня в связи с выносом обработки в main вопрос: напр., я хочу вынести из OnQuote разбор стакана в main, OnQuote будет вызывать getQuoteLevel2 и запоминать результат в циклический массив, а main будет брать его оттуда по текущему индексу, обрабатывать и освобождать память под эту табличку, которую получила через OnQuote от getQuoteLevel2. Не хочется использовать медленные sinsert и sremove. Можно ли сделать такую схему, чтобы не было конфликтов в связи с параллельным использованием данных между потоком main и потоком коллбэков?
Синхронизируйте работу потоков с этой таблицей.  Собственно это и сделано в функциях sinsert и sremove.
Чтобы функции работали быстрее, вставляйте в конец и удаляйте последний.  
Но они работают быстро даже если вставлять и удалять в любое место.
 
Николз может все таки ответишь в этой теме на вопросы заданные тебе ?  https://forum.quik.ru/messages/forum10/message75159/topic8528/#message75159

А то мы все много раз читали в твоих сообщениях что ты  доктор технических наук а каких именно даже в догадках не можем  предположить . И про созданный тобой  в 1975 году и  в одиночку ИИ ( над которым по прежнему бьются все компании программистов мирового уровня )  признайся что это были твои враки , ну типа переборщил ты в самовосхвалении .  Иначе дай нам посмотреть на этот твой ИИ от 1975 года выпуска.  
 
Цитата
БорисД написал:
Николз может все таки ответишь в этой теме на вопросы заданные тебе ?   https://forum.quik.ru/messages/forum10/message75159/topic8528/#message75159

А то мы все много раз читали в твоих сообщениях что ты  доктор технических наук а каких именно даже в догадках не можем  предположить . И про созданный тобой  в 1975 году и  в одиночку ИИ ( над которым по прежнему бьются все компании программистов мирового уровня )  признайся что это были твои враки , ну типа переборщил ты в самовосхвалении .  Иначе дай нам посмотреть на этот твой ИИ от 1975 года выпуска.  
Мое сообщение было направлено конкретному человеку.
---------------------
Удовлетворять больное любопытство хамов нет желания.
Страницы: 1
Читают тему
Наверх