web-dev-qa-db-fra.com

Comment puis-je chronométrer une pipe?

Je veux time une commande qui se compose de deux commandes distinctes avec une sortie de tuyauterie à l'autre. Par exemple, considérez les deux scripts ci-dessous:

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

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

Maintenant, comment puis-je demander à time de signaler le temps pris par foo.sh | bar.sh (et oui, je sais que la pipe n'a aucun sens ici, mais ce n'est qu'un exemple)? Cela fonctionne comme prévu si je les exécute séquentiellement dans un sous-shell sans tuyauterie:

$ time ( foo.sh; bar.sh )

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

Mais je ne peux pas le faire fonctionner lors de la tuyauterie:

$ 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

J'ai lu une question similaire ( Comment exécuter le temps sur plusieurs commandes ET écrire la sortie de temps dans un fichier? ) et j'ai également essayé l'exécutable autonome time:

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

Cela ne fonctionne même pas si je crée un troisième script qui exécute uniquement le canal:

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

Et puis le temps que:

$ time baz.sh

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

Fait intéressant, il ne semble pas que time se termine dès que la première commande est exécutée. Si je change bar.sh à:

#!/bin/sh
sleep 2
seq 1 5

Et puis time à nouveau, je m'attendais à ce que la sortie de time soit imprimée avant le seq mais ce n'est pas le cas:

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

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

Il semble que time ne compte pas le temps nécessaire pour exécuter bar.sh malgré l'attente de la fin avant d'imprimer son rapport1.

Tous les tests ont été exécutés sur un système Arch et en utilisant la version bash 4.4.12 (1). Je ne peux utiliser que bash pour le projet dont il fait partie, même si zsh ou un autre shell puissant peut le contourner, ce ne sera pas une solution viable pour moi.

Alors, comment puis-je obtenir le temps nécessaire à l'exécution d'un ensemble de commandes dirigées? Et pendant que nous y sommes, pourquoi ça ne marche pas? Il semble que time se ferme immédiatement dès que la première commande est terminée. Pourquoi?

Je sais que je peux obtenir les temps individuels avec quelque chose comme ça:

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

Mais je voudrais quand même savoir s'il est possible de chronométrer le tout en une seule opération.


1Cela ne semble pas être un problème de tampon, j'ai essayé d'exécuter les scripts avec unbuffered et stdbuf -i0 -o0 -e0 et les chiffres étaient toujours imprimés avant la sortie time.

28
terdon

Il fonctionne .

Les différentes parties d'un pipeline sont exécutées simultanément. La seule chose qui synchronise/sérialise les processus dans le pipeline est IO, c'est-à-dire qu'un processus écrit dans le processus suivant dans le pipeline et le processus suivant lit ce que le premier écrit. En dehors de cela, ils s'exécutent indépendamment les uns des autres.

Puisqu'il n'y a pas de lecture ou d'écriture entre les processus de votre pipeline, le temps nécessaire pour exécuter le pipeline est celui de l'appel sleep le plus long.

Vous pourriez aussi bien avoir écrit

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

Terdon a posté quelques exemples de scripts légèrement modifiés dans le chat :

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

et

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

La requête était "pourquoi time ( sh foo.sh | sh bar.sh ) retourne 4 secondes au lieu de 3 + 3 = 6 secondes?"

Pour voir ce qui se passe, y compris l'heure approximative à laquelle chaque commande est exécutée, on peut le faire (la sortie contient mes annotations):

$ 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

Donc, pour conclure, le pipeline prend 4 secondes, pas 6, en raison de la mise en mémoire tampon de la sortie des deux premiers appels à echo dans foo.sh.

35
Kusalananda

Serait-ce un meilleur exemple?

$ 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

Les scripts busyloop pendant 3 et 4 secondes (resp.), Ce qui prend un total de 4 secondes en temps réel en raison de l'exécution parallèle, et 7 secondes de temps CPU. (au moins environ.)

Ou ca:

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

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

Ceux-ci ne fonctionnent pas en parallèle, donc le temps total pris est de 5 secondes. Tout est passé à dormir, donc pas de temps CPU utilisé.

10
ilkkachu

Si vous avez sysdig, vous pouvez insérer des traceurs à des points arbitraires, en supposant que vous pouvez modifier le code pour ajouter les écritures nécessaires à /dev/null

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

(mais cela échoue à votre exigence "d'opération unique"), puis enregistrez les choses via

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

puis vous aurez probablement besoin d'un burin sysdig pour exporter uniquement les duré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

qui une fois enregistré dans votre sysdig/chisels répertoire comme fichier spantagduration.lua peut être utilisé comme

$ sysdig -r blalog -c spantagduration
...

Ou vous pouvez jouer avec csysdig ou la sortie JSON.

3
thrig

Trop amusant je sais que c'est un vieux fil mais je me suis retrouvé dans la même situation. J'ai découvert que les alias sont une solution de contournement simple. Au moins, cela fonctionne pour bash et poisson. Pas sûr de tous les obus.

Au lieu de: time ( foo.sh | bar.sh )

Essayer:

alias foobar="foo.sh | bar.sh" time foobar

0
BoeroBoy