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!

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

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

Комментарии (1)


  1. Pirogzas
    19.06.2024 16:46

    KUI - огонь :)