Enlarge your BASHUI for free! Как увеличить потенциал производительности?

от автора

BASHUI

BASHUI

Выдалась свободная минутка и я решил потрогать немного свой bashui. Там еще трогать не перетрогать но обо всем по порядку. Тех кто не знаком с bashui прошу сюда. А в этой статье я решил затронуть злободневную тему повышения потенциала производительности на примере своего bashui.

Одним из основных элементов bashui является меню (items) — это «табличка» с произвольным количеством строк/столбцов для отображения/выбора какого-то набора элементов. Например списка хостов/команд как в demo_sshto или списка неймспейсов/подов и других k8s элементов как в demo_kubectl, любая текстовая информация которую необходимо как-то вертеть на bashui. Я уже не молод но все хочется какой-то пестрятины, разноцветных свистоперделок каких-то. В меню (items) это есть. Я добавил возможность «раскрашивать» как заголовки так и элементы данных. Но за все приходится платить. И плата порой чрезвычайно высока. За красивую картинку приходится платить потенциалом производительности, мда, никогда такого не было и вот опять.

Давайте посмотрим как это выглядит и по возможности попробуем усилить наш потенциал. Для теста производительности я подготовил вот такой датасет:

data=(     #-------------{ first line - column descriptions }-------------------- $red'Item name'        $blu'Item description'                 $grn'Status'     #-----------------------{ the data }----------------------------------     'first'        $BLD$ylw'Long description text'                'true'     'second'               'Description 2'                        'O_o'     'third'                'description 3'                        'false'     'fourth'         "${red}Long ${grn}description ${blu}text"    'true'     ''                     ''                                     '' $ylw'fifth'                'Description 2'                        'O_o'     'sixth'                'description 3'                        'false'     'midle'            $grn'Long description text'                'true'     'long name row2'       'Description 2'                        'O_o'     ''                     ''                                      ''     'row 3'                'description 3'                        'false'     'row1'             $blu'Long description text'                'true'     'row1'                 'Long description text'                'true'     'last'                 'Description 2'                        'O_o'     'first'        $BLD$ylw'Long description text'                'true'     'second'               'Description 2'                        'O_o'     'third'                'description 3'                        'false'     'fourth'         "${red}Long ${grn}description ${blu}text"    'true'     ''                     ''                                     '' $ylw'fifth'                'Description 2'                        'O_o'     'sixth'                'description 3'                        'false'     'midle'            $grn'Long description text'                'true'     'long name row2'       'Description 2'                        'O_o'     ''                     ''                                      ''     'row 3'                'description 3'                        'false'     'row1'             $blu'Long description text'                'true'     'row1'                 'Long description text'                'true'     'last'                 'Description 2'                        'O_o' )

Примерно 30 строк в 3 столбца, ~90 элементов, попробуем повертеть это на bashui. Запускаю тестовый скрипт и зажимаю кнопку «вниз» чтобы заставить интерфейс постоянно перерисовывать картинку:

Слева меню, справа топ -д1. Обратите внимание на самый жрущий CPU процесс — demo_menu, почти 70%. Мда, не самый лучший потенциал, да? Да. В чем дело, где я обо… что пошло не так? Давайте попробуем разобраться. Вот код функции items:

items(){     # Main items piker function     local   x=${1:-1}        # X(row)  coordinate     local   y=${2:-1}        # Y(line) coordinate     local   w=${3:-$COLUMNS} # window Width     local   h=${4:-5}        # window Height, min is 5          nclm=($5)           # Number of Columns or columns sizes in % of Width     local name=$6            # List Name     local   tc=$7            # Text Color     local   rc=$8            # boRder Color     local   gc=$9            # backGround Color     shift       9     local data=("$@")     local text last c i w z column_size=()      [[ $_currentItem_ ]] || _currentItem_=0      ((w-=x))     ((${#nclm[@]}>1)) && {         for i in ${nclm[@]}; { i=$((w*i/100)); ((i<_min_culumn_size_)) && i=$_min_culumn_size_; column_size+=($i); }         z=${column_size[@]}         w=$((${z// /+}))         nclm=${#nclm[@]}         true     } || {         column_size=$((w/nclm))         ((column_size<_min_culumn_size_)) && column_size=$_min_culumn_size_         w=$((column_size*nclm))         for ((i=1; i<nclm; i++)); { column_size+=(column_size); }     }      # Print Heading     local c1='┌' c2='┐'     [[ $name ]] && {         c1='├' c2='┤'         XY $x $y "$rc┌$(line '─' $w)┐$DEF"; ((y++))         XY $x $y "$DEF$INV$rc│$DEF$INV$tc$(center_print $w "$name")$DEF$INV$rc│$DEF" ; ((y++))     }      # Print column's titles     local row=( "${data[@]:0:nclm}" )     for r in "${!row[@]}"; {         item=${row[r]}         cs=${column_size[r]}         text+="$DEF$rc$c1$(center_print $((cs-1)) "{ $item }" '─')$DEF"         text=${text//"{ "/"{ $DEF$tc"}; text=${text//" }"/"$DEF$rc }"}; text=${text//".}"/".$DEF$rc}"}; c1='┬'     };  last='─'; ((cs==_min_culumn_size_)) && last=''; XY $x $y "$text$rc$last$c2"; ((y++))      # Print data     data=(  "${data[@]:$nclm}" )     local n=${#data[@]}     local rows_avail=$((h-4))     local rows_total=$((n/nclm))     local current_row=$((_currentItem_/nclm))     _page_=$((rows_avail-1)) # pgUP/DOWN jump calculation     ((rows_avail>rows_total)) && rows_avail=$rows_total _page_=$((rows_total-1))      j=0; ((current_row>=rows_avail)) && j=$((_currentItem_+nclm-rows_avail*nclm))     for ((i=j; i<rows_avail*nclm+j; i+=nclm)); do         row=( "${data[@]:i:nclm}" )         sel=          ((i==_currentItem_)) && {             sel=$INV             decolorizer "${row[0]}" "_target_"             _target_=(  "$_target_" "${row[@]:1}" )         }          text=         for r in "${!row[@]}"; {             item=${row[r]}             cs=${column_size[r]:-column_size}             decolorizer "$item"  decolorized_item             color=$((${#item}-${#decolorized_item}))             actual_color=${item:0:$color}             ((${#decolorized_item}>=$cs-1)) && decolorized_item="${decolorized_item:0:$[cs-5]}..." item=$decolorized_item color=0             ((r==0)) && {                  [[ $item ]] || ((color++))                  printf -v new_text "$DEF$rc│$DEF$sel$gc %s$DEF$sel$gc$tc%-$((cs-3+color))s" "$INV$BLD${decolorized_item:0:1}" "$actual_color${decolorized_item:1}"             } || printf -v new_text "$DEF$rc│$DEF$sel$gc $tc%-$((cs-2+color))s" "$item"             text+=$new_text         }         text="$text $DEF$rc│$DEF\n"         XY $x $y "$text"; ((y++))     done      # Print last line     last_line=     for cs in "${column_size[@]}"; {         printf -v tmp_line "%$((cs-1))s┴"; tmp_line=${tmp_line// /─}         last_line+=$tmp_line     }      XY $x $y "$DEF$rc└${last_line%┴*}─┘$DEF"     # Show current row out of total rows if not all rows displayed     ((rows_avail<rows_total)) && { hint="{ $((current_row+1)) of $rows_total }"; XY $((w/2+x-${#hint}/2)) $y "$hint"; } }

Невооруженным взглядом видно что тут используется вложенный цикл, он необходим для правильного отображения данных. Каждый элемент данных обесцвечивается т.к. цвет это просто доп символы из-за них длинна текста определяется неправильно. Затем происходит обрезание (О_о) эм, текста чтобы каждый элемент вписался в рамки таблицы, цвета возвращаются и строка печатается. Это и есть главный bitch бич потенциала, если таблица большая, много строк и столбцов такой алгоритм заставляет мой ноут сильно грустить. Что делать? Резать к чертовой матери. Весь этот вложенный цикл можно заменить одной (почти) командой! Как? Так:

printf -v data -- "$data_template" "${data[@]:$j:$((rows_avail*nclm))}"

Эта команда рисует всю основную таблицу, правда надо немного поколдовать до чтобы собрать $data_template и после чтобы добавить выделение и от разукрашивания пришлось отказаться в пользу быстродействия. Эх. Так красивенько было с разноцветными строчками. Но полностью выкинуть разукрашивание рука не поднялась, в новой функции я оставил header практически без изменений, это же одна строка, производительность сильно не просаживает. Вот как выглядит новая функция:

items_fast(){     # Main items piker function     local   x=${1:-1}        # X(row)  coordinate     local   y=${2:-1}        # Y(line) coordinate     local   w=${3:-$COLUMNS} # window Width     local   h=${4:-5}        # window Height, min is 5          nclm=($5)           # Number of Columns or columns sizes in % of Width     local name=$6            # List Name     local   tc=$7            # Text Color     local   rc=$8            # boRder Color     local   gc=$9            # backGround Color     shift       9     local text last sel_data sel_dummy c i w z column_size=()      [[ $_currentItem_ ]] || _currentItem_=0      ((w-=x))     ((${#nclm[@]}>1)) && {         for i in ${nclm[@]}; { i=$((w*i/100)); ((i<_min_culumn_size_)) && i=$_min_culumn_size_; column_size+=($i); }         z=${column_size[@]}         w=$((${z// /+}))         nclm=${#nclm[@]}         true     } || {         column_size=$((w/nclm))         ((column_size<_min_culumn_size_)) && column_size=$_min_culumn_size_         w=$((column_size*nclm))         for ((i=1; i<nclm; i++)); { column_size+=(column_size); }     }      # Data transformation     local titles_items=(    "${@:1:$nclm}" )     shift                          $nclm     _target_=( "${@:_currentItem_+1:nclm}" )     local data=( "${@^}" )      sel_data="${data[$_currentItem_]:0:$((column_size-3))}"     ((${#sel_data}<column_size)) && printf -v sel_data "$sel_data%$((column_size-${#sel_data}-3))s"     printf -v sel_dummy "_SD_%$((column_size-7))s"     data[$_currentItem_]="$sel_dummy"      local n=$#     local rows_avail=$((h-4))     local rows_total=$((n/nclm))     local current_row=$((_currentItem_/nclm))     _page_=$((rows_avail-1)) # pgUP/DOWN jump calculation     ((rows_avail>rows_total)) && rows_avail=$rows_total _page_=$((rows_total-1))     j=0; ((current_row>=rows_avail)) && j=$((_currentItem_+nclm-rows_avail*nclm))      # Print Heading     local c1='┌' c2='┐'     [[ $name ]] && {         local c1='├' c2='┤'         XY $x $y "$rc┌$(line '─' $w)┐$DEF"; ((y++))         XY $x $y "$DEF$INV$rc│$DEF$INV$tc$(center_print $w "$name")$DEF$INV$rc│$DEF" ; ((y++))     }      titles=     last_line=     printf -v data_template  "%$((x-1))s"     for i in ${!column_size[@]};{         cs=${column_size[i]:-column_size}          # titles preparation         titles+="$DEF$rc$c1$(center_print $((cs-1)) "{ ${titles_items[i]} }" '─')$DEF"; c1='┬'          # main data template preparation         data_template+="$DEF$rc│$DEF$gc$tc %-$((cs-3)).$((cs-3))b "          # last line preparation         printf -v  tmp_line "%$((cs-1))s┴"         tmp_line=${tmp_line// /─}         last_line+=$tmp_line     };  data_template+=" $DEF$rc│$DEF\n"         titles=${titles//"{ "/"{ $DEF$tc"}         titles=${titles//" }"/"$DEF$rc }"}         titles=${titles//".}"/".$DEF$rc}"}      # Print titles     last='─'; ((cs==_min_culumn_size_)) && last=''; XY $x $y "$titles$rc$last$c2"; ((y++))      # Print data     XY 1  $y ''     printf -v data -- "$data_template" "${data[@]:$j:$((rows_avail*nclm))}"     printf "${data/$sel_dummy/$INV${sel_data}}"      ((y+=rows_avail))      # Print last line     XY $x $y "$DEF$rc└${last_line%┴*}─┘$DEF"     # Show current row out of total rows if not all rows displayed     ((rows_avail<rows_total)) && { hint="{ $((current_row+1)) of $rows_total }"; XY $((w/2+x-${#hint}/2)) $y "$hint"; } }

Попробуем повертеть это на bashui, помогло или нет?

Всего то надо было добавить _fast к названию функции и сразу стало почти в два раза быстрей. Вот справа процесс demo_menu_fast показывает результат ~33% от CPU. Неплохо, а если продолжать увеличивать размер таблицы, добавить больше строк и столбцов старая функция будет тормозить еще сильней а _fast функция практически не почувствует изменений. Потенциал заметно вырос.

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

Почему это работает? Рассмотрим поближе команду printf. Вот выдержка из хелпа:

$ printf --help printf: printf [-v переменная] формат [аргументы]     Formats and prints ARGUMENTS under control of the FORMAT.     ...     The format is re-used as necessary to consume all of the arguments.  If     there are fewer arguments than the format requires,  extra format     specifications behave as if a zero value or null string, as appropriate,     had been supplied.

т.е. все аргументы быдут выведены согласно указанному формату, простой пример:

$ printf '%s ' one one  $ printf '%s ' one two one two  $ printf '%s ' one two three one two three $ printf '%s, ' one two three one, two, three,  $ printf '%s, %s, %s.' one two three one, two, three.

Модификаторы формата могут быть такие:
%s — строка как она есть
%b — строка с раскрытием ескейпоследовательностей (\n, \t, \r …)
%d — число
%f — число с плавающей точкой

Вот тут есть полный список.

Пример поинтересней, зададим вот такой массив:

data=( one two three four five six )

И попробуем вывести его содержимое в виде таблицы из 2х столбцов:

$ printf '%s %s\n' ${data[@]} one two three four five six

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

data=(     one   two     three four     five  six )

Добавим в формат выравнивание, самое длинное слово у нас 5 букв, значит надо выровнять все столбцы до 5 символов и палки чтобы это все выглядело как таблица:

$ printf '| %-5s | %-5b |\n' ${data[@]} | one   | two   | | three | four  | | five  | six   |

А если необходимо ограничить ширину столбцов? Это тоже можно легко сделать так:

$ printf '| %-3.3s | %-3.3b |\n' ${data[@]} | one | two | | thr | fou | | fiv | six |

В этом примере я ограничил ширину столбцов 3 символами. Тоже самое происходит c bashui в этой части:

# main data template preparation data_template+="$DEF$rc│$DEF$gc$tc %-$((cs-3)).$((cs-3))b "

В $data_template добавляется вот эта вот конструкция N (по кол-ву столбцов) раз, затем этот шаблон используется для обработки массива с данными:

printf -v data -- "$data_template" "${data[@]:$j:$((rows_avail*nclm))}"

Но тут я вывожу не на экран а в переменную $data для постобработки. Вот так одна (почти) команда может заменить тягомотный цикл. Printf вообще очень удобный инструмент для работы с текстом в bash’е. Вот еще одна полезная возможность printf. Когда надо добавить какой-то timestamp в ваш скрипт многие используют date, как-то так:

time=$(date +'%Y-%m-%d') $ echo "bla $time bla" bla 2024-06-14 bla

А с printf можно сделать так:

$ printf 'bla %(%Y-%m-%d)T bla' bla 2024-06-14 bla

Огромный потенциал.

Благодарности

Нахожусь под сильным (приятным) впечатлением от замечательной поездки в Грузию которую устроила компания Ivinco с которой я в данный момент сотрудничаю. А организовали и сделали по настоящему незабываемым наше пребывание в Грузии ребята из Provodnik‘а молодцы вообще, могут. Всем кто хочет отлично провести время в Грузии (и не только) рекомендую.

Было круто, cпасибо!

Ну вот потенциал приподняли, еще пара фраз и будем прощаться. На просторах github’а наткнулся на интересное bash творчество. Я выкладывал ссылки в своём телеграм-канале, но его читают не только лишь все а штуки, как мне кажется, достойны внимания широкой аудитории, поэтому писну тут в надежде на хабраэффект)

Рисовалка с поддержкой мыши drawin на bash’е. И классическая игра snake на bash’е.
ИМО код заслуживает внимания, посмотрите.

bash snake

bash snake

Кстати у меня в репах произошло небольшое изменение. В свое время я долго думал как назвать свою поделку для kubectl. В итоге ничего лучше kube-dialog не придумал, так и назвал. Kube-dialog это обертка kubectl команд с помощью dialog’а, аналог sshto только для k8s. А недавно меня вштырило, я придумал короткое и ёмкое название — KUI (Kubectl User Interface)! Черт, почему я сразу об этом не подумал?) Но лучше поздно чем никогда, так что вместо kube-dialog’а теперь KUI!

Творите, выдумывайте, пробуйте и не разбулькивайте! 🙂

Лайки, пальцы, на ваше усмотрение.


ссылка на оригинал статьи https://habr.com/ru/articles/818497/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *