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; irows_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=$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

Невооруженным взглядом видно что тут используется вложенный цикл, он необходим для правильного отображения данных. Каждый элемент данных обесцвечивается т.к. цвет это просто доп символы из-за них длинна текста определяется неправильно. Затем происходит обрезание (О_о) эм, текста чтобы каждый элемент вписался в рамки таблицы, цвета возвращаются и строка печатается. Это и есть главный 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; irows_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

Попробуем повертеть это на 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!

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

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

© Habrahabr.ru