Три в одном
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 и напомнить про тест
памяти:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
PROGRAM MAIN | |
VAR | |
START: BOOL; | |
FileLoad: FB_FileLoad; | |
bigBuf: ARRAY[0..35000000] OF BYTE; | |
timeCounter: UDINT; | |
GetRouterStatusInfo: FB_GetRouterStatusInfo; | |
RouterStatusInfo: ST_TcRouterStatusInfo; | |
END_VAR | |
VAR CONSTANT | |
BIG_FILENAME: STRING := '/Hard Disk/NK.BIN'; // ~32.7Mb | |
END_VAR | |
FileLoad.sPathName := BIG_FILENAME; | |
FileLoad.pReadBuff := ADR(bigBuf); | |
FileLoad.cbReadLen := SIZEOF(bigBuf); | |
FileLoad.tTimeout := T#1H; | |
FileLoad(sNetId:= '', bExecute:= START); | |
IF FileLoad.bBusy THEN | |
timeCounter := timeCounter + 1; | |
ELSE | |
START := FALSE; | |
END_IF | |
GetRouterStatusInfo(sNetId:= '', bExecute:= TRUE); | |
IF NOT GetRouterStatusInfo.bBusy THEN | |
GetRouterStatusInfo(sNetId:= '', bExecute:= FALSE); | |
RouterStatusInfo := GetRouterStatusInfo.info; | |
END_IF | |
END_PROGRAM |
Пишем свой 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 раз больше миллисекунд, поэтому —
норм. По оси абсцисс отложен размер временного буфера или размер блока
байт, которые за раз подгружаются из файла (чтение идет блоками):
Исходный код не зависящий от памяти роутера
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
PROGRAM MAIN | |
VAR_INPUT | |
pReadBuf: PVOID; | |
sPathName: STRING; | |
END_VAR | |
VAR | |
START: BOOL; | |
READ_AT_ONCE: UDINT := 65536; | |
Open: FB_FileOpen; | |
Read: FB_FileRead; | |
Close: FB_FileClose; | |
state: INT; | |
pCurrent: PVOID; | |
timeCounter: UDINT; | |
END_VAR | |
IF state >= 100 AND state <= 300 THEN | |
timeCounter := timeCounter + 1; | |
END_IF | |
CASE state OF | |
0: | |
IF START THEN | |
START := FALSE; | |
timeCounter := 0; | |
pCurrent := pReadBuf; | |
state := 100; | |
END_IF | |
100: | |
Open( | |
sNetId:= '', | |
sPathName:= sPathName, | |
nMode:= FOPEN_MODEREAD OR FOPEN_MODEBINARY, | |
ePath:= PATH_GENERIC, | |
bExecute:= TRUE | |
); | |
IF NOT open.bBusy THEN | |
Open(bExecute:= FALSE); | |
state := 200; | |
END_IF | |
200: | |
Read( | |
sNetId:= '', | |
hFile:= Open.hFile, | |
pReadBuff:= pCurrent, | |
cbReadLen:= READ_AT_ONCE, | |
bExecute:= TRUE | |
); | |
IF NOT Read.bBusy THEN | |
Read(bExecute:= FALSE); | |
IF Read.cbRead > 0 THEN | |
pCurrent := pCurrent + Read.cbRead; | |
ELSE | |
state := 300; | |
END_IF | |
END_IF | |
300: | |
Close( | |
sNetId:= '', | |
hFile:= Open.hFile, | |
bExecute:= TRUE | |
); | |
IF NOT Close.bBusy THEN | |
Close(bExecute:= FALSE); | |
state := 0; | |
END_IF | |
END_CASE | |
END_PROGRAM |