Como posso cronometrar um cachimbo?

27

Eu quero timeum comando que consiste em dois comandos separados com uma saída de tubulação para outra. Por exemplo, considere os dois scripts abaixo:

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

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

Agora, como posso timerelatar o tempo gasto foo.sh | bar.sh(e sim, eu sei que o tubo não faz sentido aqui, mas este é apenas um exemplo)? Funciona como esperado se eu executá-los sequencialmente em um subshell sem canalizar:

$ time ( foo.sh; bar.sh )

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

Mas não consigo fazê-lo funcionar durante a tubulação:

$ 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

Eu li uma pergunta semelhante ( como executar o tempo em vários comandos E gravar a saída do tempo no arquivo? ) E também tentei o timeexecutável independente :

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

Nem funciona se eu criar um terceiro script que execute apenas o pipe:

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

E então cronometre isso:

$ time baz.sh

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

Curiosamente, não parece que timesai assim que o primeiro comando é concluído. Se eu mudar bar.shpara:

#!/bin/sh
sleep 2
seq 1 5

E, timenovamente, eu esperava que a timesaída fosse impressa antes do seqmas não é:

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

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

Parece timeque não conta o tempo que levou para executar, bar.shapesar de esperar a conclusão antes de imprimir o relatório 1 .

Todos os testes foram executados em um sistema Arch e usando o bash 4.4.12 (1) - release. Só posso usar o bash para o projeto que faz parte, mesmo que zshalgum shell poderoso possa contorná-lo, isso não será uma solução viável para mim.

Então, como posso obter o tempo que um conjunto de comandos canalizados levou para executar? E, enquanto estamos nisso, por que não funciona? Parece que timesai imediatamente assim que o primeiro comando é concluído. Por quê?

Eu sei que posso obter os momentos individuais com algo assim:

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

Mas ainda gostaria de saber se é possível cronometrar a coisa toda como uma única operação.


1 Este não parece ser um problema de buffer, eu tentei executar os scripts com unbufferede stdbuf -i0 -o0 -e0e os números ainda foram impressas antes da timesaída.

terdon
fonte
Você já tentou isso com um cronômetro físico?
pericynthion
@pericynthion sim, eventualmente eu fiz. E isso também mostrou o que as respostas explicam: o tempo está realmente funcionando, mas (obviamente o suficiente e como eu deveria ter percebido) os comandos no pipeline são executados simultaneamente, de modo que o tempo gasto é essencialmente o tempo mais lento.
terdon

Respostas:

33

Ele está trabalhando.

As diferentes partes de um pipeline são executadas simultaneamente. A única coisa que sincroniza / serializa os processos no pipeline é a E / S, ou seja, um processo gravando no próximo processo no pipeline e o próximo processo lendo o que o primeiro grava. Além disso, eles estão executando independentemente um do outro.

Como não há leitura ou gravação entre os processos em seu pipeline, o tempo necessário para executá-lo é o da sleepchamada mais longa .

Você também pode ter escrito

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

Terdon postou alguns scripts de exemplo levemente modificados no chat :

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

e

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

A consulta foi "por que time ( sh foo.sh | sh bar.sh )retornar 4 segundos em vez de 3 + 3 = 6 segundos?"

Para ver o que está acontecendo, incluindo o tempo aproximado em que cada comando é executado, é possível fazer isso (a saída contém minhas anotações):

$ 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

Assim, para concluir, o gasoduto leva 4 segundos, não 6, devido ao buffer da saída das duas primeiras chamadas para echoem foo.sh.

Kusalananda
fonte
11
@terdon os valores são as somas, mas os scripts levam muito pouco tempo para o usuário e o sistema - eles apenas esperam, o que não conta (exceto no relógio de parede).
Stephen Kitt
2
Observe que alguns shells como o shell Bourne ou ksh93aguardam apenas o último componente do pipeline ( sleep 3 | sleep 1durariam 1 segundo). o shell Bourne não tem timepalavra-chave, mas ksh93, quando executado com time, todos os componentes são esperados.
Stéphane Chazelas
3
Só estou dizendo que alguém pode se surpreender ao descobrir que sleep 10 | sleep 1leva um segundo enquanto time sleep 10 | sleep 1leva 10 segundos no ksh93. No shell Bourne time sleep 10 | sleep 1, levaria um segundo, mas você obteria a saída de tempo ( sleep 10somente e de /usr/bin/time) fora do azul 9 segundos depois.
Stéphane Chazelas
11
Não se trata de guardar nada. timecronometra corretamente o pipeline, mas altera o comportamento do shell no ksh93. (sleep 10 | sleep 1)leva 1 segundo, time (sleep 10 | sleep 1)leva 10 segundos. { (sleep 10 | sleep 1); echo x; }saídas xapós 1 segundo, time { (sleep 10 | sleep 1); echo x; }saídas xapós 10 segundos. Mesmo se você colocar esse código em uma função e cronometrar a função.
Stéphane Chazelas
11
Note-se que em ksh93como em zsh( -o promptsubstaqui), você pode fazer typeset -F SECONDSpara obter um menos aproximada número de segundos (POSIX shnão tem SECONDS)
Stéphane Chazelas
10

Este seria um exemplo melhor?

$ 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

Os scripts busyloop por 3 e 4 segundos (resp.), Levando um total de 4 segundos em tempo real por causa da execução paralela e 7 segundos de tempo da CPU. (pelo menos aproximadamente.)

Ou isto:

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

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

Como não são executados em paralelo, o tempo total gasto é de 5 segundos. Tudo é gasto dormindo, portanto, não é usado tempo de CPU.

ilkkachu
fonte
3

Se você tiver, sysdigpoderá inserir rastreadores em pontos arbitrários, supondo que você possa modificar o código para adicionar as gravações necessárias em/dev/null

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

(mas isso falha no seu requisito de "operação única") e registra as coisas por meio de

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

e então você provavelmente precisará de um cinzel sysdig para exportar apenas as durações

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

que uma vez salvo em seu sysdig/chiselsdiretório como o arquivo spantagduration.luapode ser usado como

$ sysdig -r blalog -c spantagduration
...

Ou você pode brincar com csysdigou com a saída JSON.

agitar
fonte