Как я могу время трубы?

27

Я хочу timeкоманду, которая состоит из двух отдельных команд с одним выводом трубопровода к другому. Например, рассмотрим два сценария ниже:

$ cat foo.sh
#!/bin/sh
sleep 4

$ cat bar.sh
#!/bin/sh
sleep 2

Теперь, как я могу timeсообщить о потраченном времени foo.sh | bar.sh(и да, я знаю, что канал здесь не имеет смысла, но это всего лишь пример)? Это работает, как и ожидалось, если я запускаю их последовательно в подоболочке без трубопровода:

$ time ( foo.sh; bar.sh )

real    0m6.020s
user    0m0.010s
sys     0m0.003s

Но я не могу заставить его работать при обвязке:

$ time ( foo.sh | bar.sh )

real    0m4.009s
user    0m0.007s
sys     0m0.003s

$ time ( { foo.sh | bar.sh; } )

real    0m4.008s
user    0m0.007s
sys     0m0.000s

$ time sh -c "foo.sh | bar.sh "

real    0m4.006s
user    0m0.000s
sys     0m0.000s

Я прочитал аналогичный вопрос ( Как запустить время на нескольких командах И записать вывод времени в файл? ), А также попробовал автономный timeисполняемый файл:

$ /usr/bin/time -p sh -c "foo.sh | bar.sh"
real 4.01
user 0.00
sys 0.00

Это даже не работает, если я создаю третий скрипт, который запускает только канал:

$ cat baz.sh
#!/bin/sh
foo.sh | bar.sh

И тогда время, которое:

$ time baz.sh

real    0m4.009s
user    0m0.003s
sys     0m0.000s

Интересно, что он не выглядит так, как будто timeзавершается, как только первая команда выполнена. Если я изменю bar.shна:

#!/bin/sh
sleep 2
seq 1 5

И timeопять же, я ожидал, что timeвывод будет напечатан раньше, seqно это не так:

$ time ( { foo.sh | bar.sh; } )
1
2
3
4
5

real    0m4.005s
user    0m0.003s
sys     0m0.000s

Похоже time, не считается время, которое потребовалось для выполнения, bar.shнесмотря на ожидание его завершения перед печатью своего отчета 1 .

Все тесты выполнялись в системе Arch и использовали выпуск bash 4.4.12 (1). Я могу использовать bash только для проекта, частью которого он является, так что даже если zshили какая-то другая мощная оболочка сможет обойти его, это не будет для меня жизнеспособным решением.

Итак, как я могу получить время, которое потребовалось для запуска набора конвейерных команд? И, хотя мы на это, почему это не работает? Похоже, что timeсразу выходит, как только первая команда закончена. Зачем?

Я знаю, что могу получить индивидуальные времена с чем-то вроде этого:

( time foo.sh ) 2>foo.time | ( time bar.sh ) 2> bar.time

Но я все еще хотел бы знать, возможно ли рассчитать все это как одну операцию.


1 Похоже, что это не проблема с буфером, я попытался запустить сценарии с unbufferedи, stdbuf -i0 -o0 -e0и числа все еще печатались до timeвывода.

Тердон
источник
Вы пробовали это с физическим секундомером?
перицинтион
@pericynthion да, в конце концов я сделал. И это также показало, что ответы объясняют: время на самом деле работает, но (очевидно, и, как я должен был понять), команды в конвейере выполняются одновременно, поэтому время, по сути, является самым медленным.
Тердон

Ответы:

33

Он будет работать.

Различные части конвейера выполняются одновременно. Единственное, что синхронизирует / сериализует процессы в конвейере, - это IO, то есть один процесс записывает следующий процесс в конвейере, а следующий процесс читает то, что пишет первый. Кроме того, они выполняются независимо друг от друга.

Поскольку между процессами в вашем конвейере не происходит чтение или запись, время выполнения конвейера - это время самого длинного sleepвызова.

Вы могли бы также написать

time ( foo.sh & bar.sh &; wait )

Terdon опубликовал несколько слегка модифицированных примеров скриптов в чате :

#!/bin/sh
# This is "foo.sh"
echo 1; sleep 1
echo 2; sleep 1
echo 3; sleep 1
echo 4

а также

#!/bin/sh
# This is "bar.sh"
sleep 2
while read line; do
  echo "LL $line"
done
sleep 1

Запрос был «почему time ( sh foo.sh | sh bar.sh )возвращает 4 секунды, а не 3 + 3 = 6 секунд?»

Чтобы увидеть, что происходит, включая приблизительное время выполнения каждой команды, можно сделать это (вывод содержит мои аннотации):

$ time ( env PS4='$SECONDS foo: ' sh -x foo.sh | PS4='$SECONDS bar: ' sh -x bar.sh )
0 bar: sleep 2
0 foo: echo 1     ; The output is buffered
0 foo: sleep 1
1 foo: echo 2     ; The output is buffered
1 foo: sleep 1
2 bar: read line  ; "bar" wakes up and reads the two first echoes
2 bar: echo LL 1
LL 1
2 bar: read line
2 bar: echo LL 2
LL 2
2 bar: read line  ; "bar" waits for more
2 foo: echo 3     ; "foo" wakes up from its second sleep
2 bar: echo LL 3
LL 3
2 bar: read line
2 foo: sleep 1
3 foo: echo 4     ; "foo" does the last echo and exits
3 bar: echo LL 4
LL 4
3 bar: read line  ; "bar" fails to read more
3 bar: sleep 1    ; ... and goes to sleep for one second

real    0m4.14s
user    0m0.00s
sys     0m0.10s

Итак, в заключение, конвейер занимает 4 секунды, а не 6, из-за буферизации выходных данных первых двух вызовов echoin foo.sh.

Кусалананда
источник
1
@terdon значения являются суммами, но сценарии занимают очень мало пользовательского и системного времени - они просто ждут, что не считается (за исключением времени настенных часов).
Стивен Китт
2
Обратите внимание, что некоторые оболочки, например оболочка Bourne, ksh93ожидают только последнего компонента конвейера ( sleep 3 | sleep 1будет длиться 1 секунду). оболочка Bourne не имеет timeключевого слова, но ksh93при запуске timeвсе компоненты ожидаются.
Стефан
3
Я просто говорю, что можно удивиться, обнаружив, что это sleep 10 | sleep 1занимает одну секунду, а time sleep 10 | sleep 1в ksh93 - 10 секунд. В оболочке Bourne time sleep 10 | sleep 1это заняло бы одну секунду, но через 9 секунд вы получите выходной сигнал времени ( sleep 10только и только /usr/bin/time).
Стефан
1
Дело не в том, чтобы что-то охранять. timeправильно рассчитывает конвейер, но меняет поведение оболочки в ksh93. (sleep 10 | sleep 1)занимает 1 секунду, time (sleep 10 | sleep 1)занимает 10 секунд. { (sleep 10 | sleep 1); echo x; }выходы xчерез 1 секунду, time { (sleep 10 | sleep 1); echo x; }выходы xчерез 10 секунд. То же самое, если вы поместите этот код в функцию и время функции.
Стефан
1
Обратите внимание, что ksh93как вzsh ( -o promptsubstздесь), вы можете сделать, typeset -F SECONDSчтобы получить менее приблизительное количество секунд (POSIX shне имеет SECONDS)
Стефан
10

Будет ли это лучшим примером?

$ time perl -e 'alarm(3); 1 while 1;' | perl -e 'alarm(4); 1 while 1;'
Alarm clock

real    0m4.004s
user    0m6.992s
sys     0m0.004s

Сценарии заняты в течение 3 и 4 секунд (соответственно), занимая в общей сложности 4 секунды в режиме реального времени из-за параллельного выполнения и 7 секунд процессорного времени. (хотя бы приблизительно.)

Или это:

$ time ( sleep 2; echo) | ( read x; sleep 3 )

real    0m5.004s
user    0m0.000s
sys     0m0.000s

Они не работают параллельно, поэтому общее время занимает 5 секунд. Все потрачено на сон, поэтому процессорное время не используется.

ilkkachu
источник
3

Если у вас есть, sysdigвы можете вставить трассировщики в произвольные точки, при условии, что вы можете изменить код, чтобы добавить необходимые записи в/dev/null

echo '>::blah::' >/dev/null
foo.sh | bar.sh
echo '<::blah::' >/dev/null

(но это не соответствует вашему требованию «одной операции»), а затем записывать вещи через

$ sudo sysdig -w blalog "span.tags contains blah"

и тогда вам, вероятно, понадобится долото sysdig для экспорта только длительностей

description = "Exports sysdig span tag durations";
short_description = "Export span tag durations.";
category = "Tracers";

args = {}

function on_init()
    ftags = chisel.request_field("span.tags")
    flatency = chisel.request_field("span.duration")
    chisel.set_filter("evt.type=tracer and evt.dir=<")
    return true
end

function on_event()
    local tags = evt.field(ftags)
    local latency = evt.field(flatency)
    if latency then
        print(tostring(tags) .. "\t" .. tonumber(latency) / 1e9)
    end
    return true
end

который когда-то сохранен в вашем sysdig/chiselsкаталоге как файл spantagduration.luaможет быть использован как

$ sysdig -r blalog -c spantagduration
...

Или вы можете поиграть с csysdig или выводом JSON.

thrig
источник