Выдалась свободная минутка и я решил потрогать немного свой 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'е.
ИМО код заслуживает внимания, посмотрите.
Кстати у меня в репах произошло небольшое изменение. В свое время я долго думал как назвать свою поделку для kubectl. В итоге ничего лучше kube-dialog не придумал, так и назвал. Kube-dialog это обертка kubectl
команд с помощью dialog'а, аналог sshto только для k8s. А недавно меня вштырило, я придумал короткое и ёмкое название - KUI (Kubectl User Interface)! Черт, почему я сразу об этом не подумал?) Но лучше поздно чем никогда, так что вместо kube-dialog'а теперь KUI!
Творите, выдумывайте, пробуйте и не разбулькивайте! :-)
Лайки, пальцы, на ваше усмотрение.