Showing posts with label maxMemAvail. Show all posts
Showing posts with label maxMemAvail. Show all posts

July 16, 2020

FB_FileLoad и память роутера

Три в одном FB_FileLoad умеет загружать файл целиком в память. Сам по себе, другие ФБ для работы с файлами не нужны. Внутри него уже есть FB_FileOpen, FB_FileRead и FB_FileClose. Два основных вопроса (всего четыре):
  • асинхронность? Иначе говоря, долгая загрузка должна быть разбита на несколько циклов.
  • Как быстро загрузит большой файл?
  • Что с потреблением памяти?
  • Вдруг свой будет работать лучше?


Используется библиотека Tc2_System версия 3.4.22.0
Базовое время цикла: 1мс
Время цикла ПЛК задачи: 1мс
ПЛК: CX5010-1110


Увеличение памяти. Бесплатно


Память ПЛК = 512 Мб. Подсмотреть ее объем можно в панели управления:



Я пытался загрузить файл размером около 32.7Мб, а получил ошибку 0x070A (1802) ADSERR_DEVICE_NOMEMORY — не хватает памяти. Причем на старте нехватка памяти в FB_FileLoad не проверяется, а выясняется только по окончании марлезонского балету. Получается, что ФБ что-то там грузит до последнего, а потом внезапно память у него заканчивается. И судя по времени загрузки, загрузить получается, но что-то в конце не складывается, идет не так как было задумано и каменный цветок в итоге не выходит. Что с памятью?

Во первых, код захватывает память статически: ему памяти либо хватает, либо он просто не соберется. Вот что пишет компилятор:
Size of generated code: 58432 bytes
Size of global data: 35024176 bytes

Total allocated memory size for code and data: 64445200 bytes
Здесь все нормально. Возможно что-то выделяется динамически из памяти роутера? Начинаю увеличивать память роутера: при 100 Мб контроллер все еще работал, а при 300Мб вообще ничего не запустилось. Остановился на 50 Мб, должно хватить. И хватило.

Со времен статьи про New, Delete и память роутера, то есть про динамическую память произошли небольшие изменения. Объем памяти настраивается все там же: System → Real-Time → Router Memory → Configured Size [MB] - мегабайты!
...но теперь для активации необходимо не только активировать конфигурацию, но и перезагрузить контроллер. Подробнее см. как загружаются параметры конфигурации. Ну и теперь нам показывают чуть больше информации:



Когда памяти стало хватать, а файл стал загружаться без ошибок, я взял FB_GetRouterStatusInfo и стал пристально следить за памятью роутера:
Доступно всего, maxMem = 52428800

До старта копирования, maxMemAvail = 52395456
После копирования, maxMemAvail = 52395456
Тоже все нормально, но внезапно я подключил Scope и все встало на свои места. По вертикальной оси отмечены байты, отмасштабированные в тысячах, хотя мегабайты должны быть кратны 1024, поэтому есть небольшое расхождение в итоговых цифрах объема:


Вот эта просадка — в виде красной ямы, происходит в конце загрузки. Система куда-то грузит файл, затем выделяет под него память роутера, что-то делает, затем данные появляются в моем статическом массиве, и тут же освобождается память роутера. Зачем?

Загрузка длится долго, около 30 секунд (см. ниже), буфер заполняется постепенно, а итоговый массив с данными доступен весь и на всем протяжении загрузки. Возможно, именно по этой причине, от нас временно пытаются скрыть недозагруженные куски данных. Я попробовал выделить буфер динамически — через команду __NEW, ну и получил очередную нехватку памяти, так как от 50 мб осталось только 17 Мб и буфер под копирование выделять уже было не из чего.


Timeout


Если ФБ загружает долго (а большие файлы он грузит долго), то при таймауте ФБ выдает ошибку 0x0745 (1861) ADSERR_CLIENT_SYNCTIMEOUT. Но почему таймаут срабатывает через в два раза больший промежуток времени: задаешь 2сек — срабатывает через 4, задаешь 5 сек — срабатывает через 10; 10 сек → 20 сек. Причем код ошибки nErrId формируется через заданное время, а флаг ошибки bError устанавливается спустя еще один промежуток таймаута (начиная от установки кода ошибки). Итого, получаем удвоенное время. Это баг или фича?

Я поставил таймаут в один час T#1H и этого должно хватить на загрузку. И хватило.


Тестируем оригинал


Под рукой был большой файл бинарного содержания /Hard Disk/NK.BIN размером около ~32.7Mb. Загружаем его несколько раз:
= 26,078 сек.
= 26,075 сек.
= 26,095 сек.
  
Исходный код чтобы обратить внимание на таймаут в T#1h и напомнить про тест памяти:



Пишем свой FileLoad c буферами и чартами

  1 Кб = 92,368 сек
  4 Кб = 42,063 сек
  8 Кб = 33,781 сек
 16 Кб = 29,631 сек
 32 Кб = 28,251 сек
 64 Кб = 26,625 сек <<<<
128 Кб = 26,613 сек
  1 Мб = 26,179 сек
  4 Мб = 26,115 сек
По результатам строим самый настоящий график из Экселя. Ось Y конечно же не от нуля: там секунды, а они в 1000 раз больше миллисекунд, поэтому — норм. По оси абсцисс отложен размер временного буфера или размер блока байт, которые за раз подгружаются из файла (чтение идет блоками):



Исходный код не зависящий от памяти роутера

June 19, 2019

New, Delete и память роутера

Мир готов отказаться от ручного управления динамической памятью, а мы ее только-только получили, в нагрузку к TwinCAT 3. Иначе классы и прочий полиморфизм не работают. Теперь, когда появилась динамическая память, должны появиться и динамические типы данных, а если не появятся — мы их сделаем сами.

Ключевые слова


Во времена TwinCAT 2 динамической памяти не было. Если что-то изменяло свой размер, то под него выделялся буфер максимального размера, рядом ставился счетчик длины или количества элементов, или как-то еще, но в итоге как-то отмечали окончание цепочки символов. Например, нулевой символ \0 в конце строки. Как вариант, можно было создать массив структур, и по мере необходимости добавлять в него указатели, и таким образом нафантазировать себе списки, очереди, стеки, деревья, а также другие типы данных переменной длины.

Теперь же появились новые ключевые слова __NEW и __DELETE. С помощью них можно создавать переменные во время выполнения программы. Место для переменных выделяется в большой, неразмеченной куче памяти, а по окончании работы место возвращается. Можно создавать и снова удалять, и так далее без конца.

PROGRAM NewDeleteRepeatAgain
VAR
    ptr : POINTER TO ARRAY[0..4095] OF BYTE; 
END_VAR

[...]

ptr := __NEW(BYTE, 4096);
__DELETE(ptr);

Оба ключевых слова встроены в язык программирования и являются расширением стандарта IEC 61131-3. Еще раз, это не функциональные блоки, это встроенные средства языка программирования. Обе команды работают с указателями типа POINTER TO ТипДанных. С "голыми" адресами типа DINT и PVOID команды работать не умеют и тем самым достигается какая-никакая безопасность при работе с арифметикой указателей.

Кроме динамических структур, можно на лету создавать экземпляры функциональных блоков. В результате получится указатель на функциональный блок, который затем используется как обычная функция. Чтобы функциональный блок или структуру стали динамически создаваемыми, их необходимо пометить специальным атрибутом {attribute 'enable_dynamic_creation'}.


Память роутера


Память занимается у роутера. Ее объем — это обычная настройка проекта:



Роутеру же память и возвращается — если, конечно, разработчик не забудет вернуть. Если забудет, то память закончится и __NEW начнет возвращать нули вместо указателей. Я пробовал выделять блоки длиной в 4 мегабайта и тут же удалять их, и так каждый ПЛК цикл. Ни каких проблем с этим не возникло. Время работы практически не отличается от работы с фиксированными данными. Выделение и освобождение памяти происходит максимально быстро и практически не заметно. Разве что, чуть-чуть.

В эксперименте принимал участие контроллер С6015 с атомом E3845 на борту (Windows 10 IoT) и CX9020 c ARMv7 и WinCE Emb. Результаты по таймингам идентичные. Более слабый процессор CX9020 позволяет обрабатывать меньшее количество данных за цикл, поэтому размер блоков пришлось сократить до 100 килобайт, но задержки по прежнему аналогичны старшему брату. От разрядности и операционной системы тоже ничего не зависит. Вот за эту универсальность все и любят третью версию TwinCAT.

__DELETE возвращает теперь уже свободное место в кучу. С нулевыми указателями команда не работает: она просто игнорируя их. Указатели в TC3 типизированные, поэтому кроме как трюками с ADS их не испортить. Если не забывать освобождать занятую память, то все должно быть нормально. На картинке ниже, я ежесекундно занимаю по 4 мегабайта из памяти роутера, а затем... не освобождаю ее: когда-нибудь память у роутера закончится.



Самоконтроль


Библиотека Tc2_Utilities предлагает функцию FB_GetRouterStatusInfo, отвечающую за контроль состояния роутера. Поле maxMemAvail выходной структуры ST_TcRouterStatusInfo хранит текущий объем свободной памяти роутера в байтах. Эта же память является "кучей" динамической памяти.

Аналогичную информацию разработчик может получить и без программирования, просто ткнув правой кнопкой мыши в иконку TwinCAT, а затем в Router → Info.


К сожалению, в данный момент (для TwinCAT 3.1.4022.29) единственный способ очистить кучу не отключая контроллер — это перезапустить системный сервис TwinCAT. Перезапуск программы ПЛК, перезаливка конфигурации, холодный или полный сброс переменных, переключение в режим конфигурации, любые другие телодвижения — не скроют следы утечек памяти у неаккуратного разработчика. Бдите!


Производительность


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

Как там дела с обработкой элементов массива или динамического списка (vector, List, итп типов данных)? Здесь есть небольшое различие, которые не так чтобы сильно заметны и возможно это просто накладные расходы на работу с указателями, но все-таки чуть медленнее. По вертикали отложены миллисекунды от 4 до 10 мс:


Каждый цикл программа пробегает по всем элементам массива: 8 миллисекунд длится забег по статическому массиву и 9 миллисекунд по динамическому. Резкие провалы вниз — это выделение динамической памяти под массив (которые, как нам теперь известно, почти не занимает времени цикла).

Итого, не сильно большая разница чтобы осторожничать, но проверить стоило.


PS: Копаем глубже


При создании нового экземпляра происходит инициализация объекта и его полей. Если в объекте есть вложенные объекты (структура в структуре), они также будут инициализированы. С классами немного сложнее, так как в них есть конструкторы и деструкторы, но это уже отдельная тема.