Comment puis-je chronométrer une pipe?

27

Je veux timeune 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 timesignaler 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' timeexécutable autonome :

$ /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 n'apparaît pas comme s'il timequitte dès que la première commande est effectuée. Si je change bar.shpour:

#!/bin/sh
sleep 2
seq 1 5

Et là timeencore, je m'attendais à ce que la timesortie soit imprimée avant le seqmais 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

On dirait que timene compte pas le temps qu'il a fallu pour exécuter bar.shmalgré l'attente de sa fin avant d'imprimer son rapport 1 .

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 s'agit, donc même si zshun 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 timela sortie soit immédiate 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.


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

terdon
la source
L'avez-vous essayé avec un chronomètre physique?
péricynthion
@pericynthion yep finalement je l'ai fait. Et cela a également montré ce que les réponses expliquent: le temps fonctionne réellement mais (évidemment assez et comme je l'aurais dû réaliser) les commandes dans le pipeline s'exécutent simultanément, donc le temps pris est essentiellement le temps le plus lent.
terdon

Réponses:

33

Il est travaille.

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' sleepappel le plus long .

Vous pourriez aussi bien avoir écrit

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

Terdon a publié 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 question é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 vers echoin foo.sh.

Kusalananda
la source
1
@terdon les valeurs sont les sommes, mais les scripts prennent très peu de temps utilisateur et système - ils attendent juste, ce qui ne compte pas (sauf en temps d'horloge murale).
Stephen Kitt
2
Notez que certains shells comme le shell Bourne ou ksh93n'attendent que le dernier composant du pipeline ( sleep 3 | sleep 1durerait 1 seconde). le shell Bourne n'a pas de timemot-clé, mais dans ksh93, lorsqu'il est exécuté avec time, tous les composants sont attendus.
Stéphane Chazelas
3
Je dis juste que l'on peut être surpris de constater que cela sleep 10 | sleep 1prend une seconde tout time sleep 10 | sleep 1en 10 secondes en ksh93. Dans le shell Bourne, time sleep 10 | sleep 1cela prendrait une seconde, mais vous obtiendriez le temps de sortie (pour sleep 10seulement et à partir de /usr/bin/time) du bleu 9 secondes plus tard.
Stéphane Chazelas
1
Il ne s'agit pas de garder quoi que ce soit. timechronomètre correctement le pipeline, mais modifie le comportement du shell dans ksh93. (sleep 10 | sleep 1)prend 1 seconde, time (sleep 10 | sleep 1)prend 10 secondes. { (sleep 10 | sleep 1); echo x; }sorties xaprès 1 seconde, time { (sleep 10 | sleep 1); echo x; }sorties xaprès 10 secondes. Même chose si vous mettez ce code dans une fonction et chronométrez la fonction.
Stéphane Chazelas
1
Notez que dans ksh93comme dans zsh( -o promptsubstici), vous pouvez faire typeset -F SECONDSpour obtenir un nombre de secondes moins approximatif (POSIX shn'en a pas SECONDS)
Stéphane Chazelas
10

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é.

ilkkachu
la source
3

Si vous en avez, sysdigvous 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/chiselsrépertoire en tant que fichier spantagduration.luapeut être utilisé comme

$ sysdig -r blalog -c spantagduration
...

Ou vous pouvez jouer avec csysdigou la sortie JSON.

branler
la source