【问题标题】:How can I profile a Bash shell script slow startup?如何分析 Bash shell 脚本启动缓慢?
【发布时间】:2011-06-28 05:46:45
【问题描述】:

我的 Bash shell 最多需要 3-4 秒才能启动,而如果我使用 --norc 启动它,它会立即运行。

我通过手动插入return 语句并寻求速度改进开始“分析”/etc/bash.bashrc~/.bashrc,但这不是一个量化的过程,效率不高。

如何分析我的 Bash 脚本,并查看哪些命令执行时间最长?

【问题讨论】:

  • 我分析了脚本,大部分时间都花在了 bash_completion 的设置上。
  • 这并不奇怪,因为它相当大。如果您想麻烦维护跨更新等的更改,您可以通过删除您知道永远不需要的部分来加快速度。
  • 您可以比较:time bash -c 'exit'time bash -i -c 'exit',还可以与 --norc--noprofile 进行比较。
  • 另请参阅此答案(免责声明:这是我的)。不完全是您要问的,但绝对相关:unix.stackexchange.com/a/555510/384864
  • 根据人们需要的“高效”机制,您可以在看起来很昂贵的命令之前/之后添加类似echo $EPOCHREALTIME(对于 bash > 版本 5)(与 bash_completion、pyenv 相关的任何内容) , 等等)。 stackoverflow.com/a/67044674/1024811

标签: bash profiling


【解决方案1】:

如果您有 GNU date(或其他可以输出纳秒的版本),请在 /etc/bash.bashrc 的开头执行此操作(或您希望在任何 Bash 脚本中开始跟踪的任何位置):

PS4='+ $(date "+%s.%N")\011 '
exec 3>&2 2>/tmp/bashstart.$$.log
set -x

添加

set +x
exec 2>&3 3>&-

~/.bashrc 的末尾(或在您希望停止跟踪的任何 Bash 脚本部分的末尾)。 \011 是八进制制表符。

您应该在/tmp/bashstart.PID.log 中获得一个跟踪日志,其中显示了每个已执行命令的 seconds.nanoseconds 时间戳。从一次到下一次的差异是干预步骤所花费的时间。

当您缩小范围时,您可以将set -x 移到后面,将set +x 移到前面(或有选择地将几个感兴趣的部分括起来)。

虽然它不像 GNU date 的纳秒那样细粒度,但 Bash 5 包含一个以微秒为单位给出时间的变量。使用它可以避免为每一行生成一个外部可执行文件,并且可以在没有 GNU date 的 Mac 或其他地方工作——当然,只要你有 Bash 5。更改PS4的设置:

PS4='+ $EPOCHREALTIME\011 '

正如@pawamoy 所指出的,如果您有 Bash 4.1 或更高版本,您可以使用 BASH_XTRACEFD 将跟踪的输出发送到单独的文件描述符。来自this answer

#!/bin/bash

exec 5> command.txt
BASH_XTRACEFD="5"

echo -n "hello "

set -x
echo -n world
set +x

echo "!"

这将导致跟踪输出转到文件command.txt 留下stdoutstdout 正常输出(或单独重定向)。

【讨论】:

  • shell 提示符不可见并且我的命令没有回显是否正常?但是,我得到了跟踪,所以我可以开始分析了.. 非常感谢!
  • @AndreaSpadaccini:最后的exec 应该让 fd2 恢复正常,所以你应该得到提示。
  • ...实际上,使用 bash 4.2,可以做得更好——在PS4 中使用\D{...} 允许扩展完全任意的时间格式字符串,而不会产生启动date 的性能开销作为一个子进程。
  • @CharlesDuffy:这些都非常酷。然而 GNU date 理解 %N 而 Bash 4.2 在 GNU 系统上不理解(因为 strftime(3) 不理解) - 如此随意有限制。您关于性能与分辨率的观点是一个很好的观点,用户应该明智地做出选择,请记住,性能影响仅在调试期间是暂时的(并且仅在set -x 生效时)。
  • 使用 Bash 4,还可以使用 BASH_XTRACEFD 变量将调试输出重定向到另一个文件描述符,而不是默认的文件描述符(2 或 stderr)。当需要分析输出(分析数据)时,它非常有帮助,因为不必再解开 stderr 并设置 -x 输出(如此多的边缘情况)。
【解决方案2】:

剖析Bash(四个答案)

阅读本文,因为 分析 是重要的一步,我已经对整个 Stack Overflow 问题进行了一些测试和研究,并且已经发布了答案。

答案不止四个:

  • 第一个是基于@DennisWilliamson 的想法,但是资源消耗要少很多

  • 第二个是我自己的(在此之前;)

  • 第三个是基于@fgm 的回答,但更准确。

  • 最后使用scriptscriptreplay定时文件

  • 最后来个性能对比。

使用set -xdate,但分叉

借鉴@DennisWilliamson 的想法,但使用以下语法,三个命令只有一个初始分叉:

exec 3>&2 2> >(tee /tmp/sample-time.$$.log |
                 sed -u 's/^.*$/now/' |
                 date -f - +%s.%N >/tmp/sample-time.$$.tim)
set -x

这样做只会运行一次date。有一个快速演示/测试来展示它是如何工作的:

for i in {1..4};do echo now;sleep .05;done| date -f - +%N

示例脚本:

#!/bin/bash

exec 3>&2 2> >( tee /tmp/sample-$$.log |
                  sed -u 's/^.*$/now/' |
                  date -f - +%s.%N >/tmp/sample-$$.tim)
set -x

for ((i=3;i--;));do sleep .1;done

for ((i=2;i--;))
do
    tar -cf /tmp/test.tar -C / bin
    gzip /tmp/test.tar
    rm /tmp/test.tar.gz
done

set +x
exec 2>&3 3>&-

通过运行此脚本,您可以创建 2 个文件:/tmp/sample-XXXX.log/tmp/sample-XXXX.tim(其中 XXXX 是运行脚本的进程 ID)。

您可以使用paste 来展示它们:

paste tmp/sample-XXXX.{tim,log}

或者你甚至可以计算差异时间:

paste <(
    while read tim ;do
        crt=000000000$((${tim//.}-10#0$last))
        printf "%12.9f\n" ${crt:0:${#crt}-9}.${crt:${#crt}-9}
        last=${tim//.}
      done < sample-time.24804.tim
  ) sample-time.24804.log

 1388487534.391309713        + (( i=3 ))
 0.000080807        + (( i-- ))
 0.000008312        + sleep .1
 0.101304843        + (( 1 ))
 0.000032616        + (( i-- ))
 0.000007124        + sleep .1
 0.101251684        + (( 1 ))
 0.000033036        + (( i-- ))
 0.000007054        + sleep .1
 0.104013813        + (( 1 ))
 0.000026959        + (( i-- ))
 0.000006915        + (( i=2 ))
 0.000006635        + (( i-- ))
 0.000006844        + tar -cf /tmp/test.tar -C / bin
 0.022655107        + gzip /tmp/test.tar
 0.637042668        + rm /tmp/test.tar.gz
 0.000823649        + (( 1 ))
 0.000011314        + (( i-- ))
 0.000006915        + tar -cf /tmp/test.tar -C / bin
 0.016084482        + gzip /tmp/test.tar
 0.627798263        + rm /tmp/test.tar.gz
 0.001294946        + (( 1 ))
 0.000023187        + (( i-- ))
 0.000006845        + set +x

或在两列上:

paste <(
    while read tim ;do
        [ -z "$last" ] && last=${tim//.} && first=${tim//.}
        crt=000000000$((${tim//.}-10#0$last))
        ctot=000000000$((${tim//.}-10#0$first))
        printf "%12.9f %12.9f\n" ${crt:0:${#crt}-9}.${crt:${#crt}-9} \
                                 ${ctot:0:${#ctot}-9}.${ctot:${#ctot}-9}
        last=${tim//.}
      done < sample-time.24804.tim
  ) sample-time.24804.log

可能呈现:

 0.000000000  0.000000000   + (( i=3 ))
 0.000080807  0.000080807   + (( i-- ))
 0.000008312  0.000089119   + sleep .1
 0.101304843  0.101393962   + (( 1 ))
 0.000032616  0.101426578   + (( i-- ))
 0.000007124  0.101433702   + sleep .1
 0.101251684  0.202685386   + (( 1 ))
 0.000033036  0.202718422   + (( i-- ))
 0.000007054  0.202725476   + sleep .1
 0.104013813  0.306739289   + (( 1 ))
 0.000026959  0.306766248   + (( i-- ))
 0.000006915  0.306773163   + (( i=2 ))
 0.000006635  0.306779798   + (( i-- ))
 0.000006844  0.306786642   + tar -cf /tmp/test.tar -C / bin
 0.022655107  0.329441749   + gzip /tmp/test.tar
 0.637042668  0.966484417   + rm /tmp/test.tar.gz
 0.000823649  0.967308066   + (( 1 ))
 0.000011314  0.967319380   + (( i-- ))
 0.000006915  0.967326295   + tar -cf /tmp/test.tar -C / bin
 0.016084482  0.983410777   + gzip /tmp/test.tar
 0.627798263  1.611209040   + rm /tmp/test.tar.gz
 0.001294946  1.612503986   + (( 1 ))
 0.000023187  1.612527173   + (( i-- ))
 0.000006845  1.612534018   + set +x

最近 GNU/Linux 内核上使用trap debug/proc/timer_list没有 forks

GNU/Linux 的最新内核下,您可能会找到一个名为 timer_list/proc 文件:

grep 'now at\|offset' /proc/timer_list
now at 5461935212966259 nsecs
  .offset:     0 nsecs
  .offset:     1383718821564493249 nsecs
  .offset:     0 nsecs

其中当前时间是5461935212966259 + 1383718821564493249 的总和,但以纳秒为单位。

所以对于计算经过的时间,不需要知道偏移量。

对于这类工作,我写了elap.bash (V2),其来源如下:

source elap.bash-v2

. elap.bash-v2 init

(完整语法见 cmets)

所以你可以简单地在你的脚本顶部添加这一行:

. elap.bash-v2 trap2

一个小样本:

#!/bin/bash

. elap.bash-v2 trap

for ((i=3;i--;));do sleep .1;done

elapCalc2
elapShowTotal \\e[1mfirst total\\e[0m

for ((i=2;i--;))
do
    tar -cf /tmp/test.tar -C / bin
    gzip /tmp/test.tar
    rm /tmp/test.tar.gz
done

trap -- debug
elapTotal \\e[1mtotal time\\e[0m

在我的主机上渲染:

 0.000947481 Starting
 0.000796900 ((i=3))
 0.000696956 ((i--))
 0.101969242 sleep .1
 0.000812478 ((1))
 0.000755067 ((i--))
 0.103693305 sleep .1
 0.000730482 ((1))
 0.000660360 ((i--))
 0.103565001 sleep .1
 0.000719516 ((1))
 0.000671325 ((i--))
 0.000754856 elapCalc2
 0.316018113 first total
 0.000754787 elapShowTotal \e[1mfirst total\e[0m
 0.000711275 ((i=2))
 0.000683408 ((i--))
 0.075673816 tar -cf /tmp/test.tar -C / bin
 0.596389329 gzip /tmp/test.tar
 0.006565188 rm /tmp/test.tar.gz
 0.000830217 ((1))
 0.000759466 ((i--))
 0.024783966 tar -cf /tmp/test.tar -C / bin
 0.604119903 gzip /tmp/test.tar
 0.005172940 rm /tmp/test.tar.gz
 0.000952299 ((1))
 0.000827421 ((i--))
 1.635788924 total time
 1.636657204 EXIT

使用trap2 而不是trap 作为source 命令的参数:

#!/bin/bash

. elap.bash-v2 trap2
...

将呈现两列last command 和 total

 0.000894541      0.000894541 Starting
 0.001306122      0.002200663 ((i=3))
 0.001929397      0.004130060 ((i--))
 0.103035812      0.107165872 sleep .1
 0.000875613      0.108041485 ((1))
 0.000813872      0.108855357 ((i--))
 0.104954517      0.213809874 sleep .1
 0.000900617      0.214710491 ((1))
 0.000842159      0.215552650 ((i--))
 0.104846890      0.320399540 sleep .1
 0.000899082      0.321298622 ((1))
 0.000811708      0.322110330 ((i--))
 0.000879455      0.322989785 elapCalc2
 0.322989785 first total
 0.000906692      0.323896477 elapShowTotal \e[1mfirst total\e[0m
 0.000820089      0.324716566 ((i=2))
 0.000773782      0.325490348 ((i--))
 0.024752613      0.350242961 tar -cf /tmp/test.tar -C / bin
 0.596199363      0.946442324 gzip /tmp/test.tar
 0.003007128      0.949449452 rm /tmp/test.tar.gz
 0.000791452      0.950240904 ((1))
 0.000779371      0.951020275 ((i--))
 0.030519702      0.981539977 tar -cf /tmp/test.tar -C / bin
 0.584155405      1.565695382 gzip /tmp/test.tar
 0.003058674      1.568754056 rm /tmp/test.tar.gz
 0.000955093      1.569709149 ((1))
 0.000919964      1.570629113 ((i--))
 1.571516599 total time
 0.001723708      1.572352821 EXIT

使用strace

是的,strace 可以胜任:

strace -q -f -s 10 -ttt sample-script 2>sample-script-strace.log

但它可以做很多东西!

wc sample-script-strace.log
    6925  57637 586518 sample-script-strace.log

使用更受限制的命令:

strace -f -s 10 -ttt -eopen,access,read,write ./sample-script 2>sample-script-strace.log

将转储较轻的日志:

  4519  36695 374453 sample-script-strace.log

根据您要搜索的内容,您可能会更加严格:

 strace -f -s 10 -ttt -eaccess,open ./sample-script 2>&1 | wc
  189    1451   13682

阅读它们会有点困难:

{
    read -a first
    first=${first//.}
    last=$first
    while read tim line;do
        crt=000000000$((${tim//.}-last))
        ctot=000000000$((${tim//.}-first))
        printf "%9.6f %9.6f %s\n" ${crt:0:${#crt}-6}.${crt:${#crt}-6} \
            ${ctot:0:${#ctot}-6}.${ctot:${#ctot}-6} "$line"
        last=${tim//.}
      done
  } < <(
    sed </tmp/sample-script.strace -e '
        s/^ *//;
        s/^\[[^]]*\] *//;
        /^[0-9]\{4\}/!d
  ')

 0.000110  0.000110 open("/lib/x86_64-linux-gnu/libtinfo.so.5", O_RDONLY) = 4
 0.000132  0.000242 open("/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY) = 4
 0.000121  0.000363 open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY) = 4
 0.000462  0.000825 open("/dev/tty", O_RDWR|O_NONBLOCK) = 4
 0.000147  0.000972 open("/usr/lib/locale/locale-archive", O_RDONLY) = 4
 ...
 0.000793  1.551331 open("/etc/ld.so.cache", O_RDONLY) = 4
 0.000127  1.551458 open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY) = 4
 0.000545  1.552003 open("/usr/lib/locale/locale-archive", O_RDONLY) = 4
 0.000439  1.552442 --- SIGCHLD (Child exited) @ 0 (0) ---

原来的 bash 脚本在这个中不太容易理解...

使用scriptscriptreplay计时文件

作为 BSD Utils 的一部分,script(和scriptreplay)是一个非常古老的工具,可用于分析 bash,占用空间非常小。

script -t script.log 2>script.tim -c 'bash -x -c "
    for ((i=3;i--;));do sleep .1;done

    for ((i=2;i--;)) ;do
        tar -cf /tmp/test.tar -C / bin
        gzip /tmp/test.tar
        rm /tmp/test.tar.gz
    done
"'

将产生:

Script started on Fri Mar 25 08:29:37 2016
+ (( i=3 ))
+ (( i-- ))
+ sleep .1
+ (( 1 ))
+ (( i-- ))
+ sleep .1
+ (( 1 ))
+ (( i-- ))
+ sleep .1
+ (( 1 ))
+ (( i-- ))
+ (( i=2 ))
+ (( i-- ))
+ tar -cf /tmp/test.tar -C / bin
+ gzip /tmp/test.tar
+ rm /tmp/test.tar.gz
+ (( 1 ))
+ (( i-- ))
+ tar -cf /tmp/test.tar -C / bin
+ gzip /tmp/test.tar
+ rm /tmp/test.tar.gz
+ (( 1 ))
+ (( i-- ))
Script done on Fri Mar 25 08:29:39 2016

并生成两个文件:

ls -l script.*
-rw-r--r-- 1 user user 450 Mar 25 08:29 script.log
-rw-r--r-- 1 user user 177 Mar 25 08:29 script.tim

文件script.log 包含所有轨迹,script.tim计时文件

head -n 4 script.*
==> script.log <==
Script started on Fri Mar 25 08:29:37 2016
+ (( i=3 ))
+ (( i-- ))
+ sleep .1

==> script.tim <==
0.435331 11
0.000033 2
0.000024 11
0.000010 2

您可以通过日志文件的第一行和最后一行和/或通过在计时文件中汇总时间来查看总执行时间:

head -n1 script.log ;tail -n1 script.log
Script started on Fri Mar 25 08:29:37 2016
Script done on Fri Mar 25 08:29:39 2016

sed < script.tim  's/ .*$//;H;${x;s/\n/+/g;s/^\+//;p};d' | bc -l
2.249755

在计时文件中,第二个值是对应日志文件中接下来的字节数。这让您能够重播日志文件,可选择使用加速因子

scriptreplay script.{tim,log}

scriptreplay script.{tim,log} 5

 scriptreplay script.{tim,log} .2

并排显示时间和命令也有点复杂:

exec 4<script.log
read -u 4 line
echo $line ;while read tim char;do
    read -u 4 -N $char -r -s line
    echo $tim $line
  done < script.tim &&
while read -u 4 line;do
    echo $line
done;exec 4<&-
Script started on Fri Mar 25 08:28:51 2016
0.558012 + (( i=3 ))
0.000053
0.000176 + (( i-- ))
0.000015
0.000059 + sleep .1
0.000015
 + sleep .1) + (( 1 ))
 + sleep .1) + (( 1 ))
 + tar -cf /tmp/test.tar -C / bin
0.035024 + gzip /tmp/test.tar
0.793846 + rm /tmp/test.tar.gz
 + tar -cf /tmp/test.tar -C / bin
0.024971 + gzip /tmp/test.tar
0.729062 + rm /tmp/test.tar.gz
 + (( i-- )) + (( 1 ))
Script done on Fri Mar 25 08:28:53 2016

测试和结论

为了进行测试,我在Bash complex Hello, World! 下载了第二个示例。在我的主机上完成此脚本大约需要 0.72 秒。

我在脚本顶部添加了以下之一:

  • 通过elap.bash函数

     #!/bin/bash
    
     source elap.bash-v2 trap2
    
     eval "BUNCHS=(" $(perl <<EOF | gunzip
     ...
    
  • set -xPS4

     #!/bin/bash
    
     PS4='+ $(date "+%s.%N")\011 '
     exec 3>&2 2>/tmp/bashstart.$$.log
     set -x
    
     eval "BUNCHS=(" $(perl <<EOF | gunzip
     ...
    
  • set -xinitial fork to long exec 命令

     #!/bin/bash
    
     exec 3>&2 2> >(tee /tmp/sample-time.$$.log |
                      sed -u 's/^.*$/now/' |
                      date -f - +%s.%N >/tmp/sample-time.$$.tim)
     set -x
    
     eval "BUNCHS=(" $(perl <<EOF | gunzip
    
  • script(和set +x

     script -t helloworld.log 2>helloworld.tim -c '
         bash -x complex_helloworld-2.sh' >/dev/null
    

并比较执行时间(在我的主机上):

  • 直接 0.72 秒
  • elap.bash 13.18 秒
  • 设置 + 日期@PS4 54.61 秒
  • 设置 + 1 个分叉 1.45 秒
  • 脚本和计时文件 2.19 秒
  • strace 4.47 秒

输出

  • elap.bash 函数
         0.000950277      0.000950277 Starting
         0.007618964      0.008569241 eval "BUNCHS=(" $(perl <<EOF | gunzi
         0.005259953      0.013829194 BUNCHS=("2411 1115 -13 15 33 -3 15 1
         0.010945070      0.024774264 MKey="V922/G/,2:"
         0.001050990      0.025825254 export RotString=""
         0.004724348      0.030549602 initRotString
         0.001322184      0.031871786 for bunch in "${BUNCHS[@]}"
         0.000768893      0.032640679 out=""
         0.001008242      0.033648921 bunchArray=($bunch)
         0.000741095      0.034390016 ((k=0))
  • set -xPS4
    ++ 1388598366.536099290  perl
    ++ 1388598366.536169132  gunzip
    + 1388598366.552794757   eval 'BUNCHS=(' '"2411' 1115 -13 15 33 -3 15 1
    ++ 1388598366.555001983  BUNCHS=("2411 1115 -13 15 33 -3 15 13111 -6 1
    + 1388598366.557551018   MKey=V922/G/,2:
    + 1388598366.558316839   export RotString=
    + 1388598366.559083848   RotString=
    + 1388598366.560165147   initRotString
    + 1388598366.560942633   local _i _char
    + 1388598366.561706988   RotString=
  • set -xinitial fork to long exec 命令(以及我的第二个 paste 示例脚本)
     0.000000000  0.000000000    ++ perl
     0.008141159  0.008141159    ++ gunzip
     0.000007822  0.008148981    + eval 'BUNCHS=(' '"2411' 1115 -13 15 33 -3
     0.000006216  0.008155197    ++ BUNCHS=("2411 1115 -13 15 33 -3 15 13111
     0.000006216  0.008161413    + MKey=V922/G/,2:
     0.000006076  0.008167489    + export RotString=
     0.000006007  0.008173496    + RotString=
     0.000006006  0.008179502    + initRotString
     0.000005937  0.008185439    + local _i _char
     0.000006006  0.008191445    + RotString=
  • strace
     0.000213  0.000213 brk(0)                = 0x17b6000
     0.000044  0.000257 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
     0.000047  0.000304 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7faf1c0dc000
     0.000040  0.000344 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
     0.000040  0.000384 open("/etc/ld.so.cache", O_RDONLY) = 4
     ...
     0.000024  4.425049 close(10)             = 0
     0.000042  4.425091 rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
     0.000028  4.425119 read(255, "", 4409)   = 0
     0.000058  4.425177 exit_group(0)         = ?
  • script
    Le script a débuté sur ven 25 mar 2016 09:18:35 CET
    0.667160 ++ gunzip
    0.000025
    0.000948 ++ perl
    0.000011
    0.005338 + eval 'BUNCHS=(' '"2411' 1115 -13 15 33 -3 15 13111 -6 1 111 4
    0.000044 1223 15 3311 121121 17 3311 121121 1223 3311 121121 17 3311 121
    0.000175 ++ BUNCHS=("2411 1115 -13 15 33 -3 15 13111 -6 15 1114 15 12211
    0.000029 1 1321 12211 412 21211 33 21211 -2 15 2311 11121 232 121111 122
    0.000023 4 3311 121121 12221 3311 121121 12221 3311 121121 1313 -6 15 33

结论

好吧!如果我的 pure Bash 脚本比 在每个命令上分叉到日期 更快,那么我的纯 Bash 脚本意味着对每个命令进行一些操作。

为日志和存储专门提供一个独立进程的方式显然更有效。

strace 是一种有趣的方式,更详细,但难以阅读。

scriptscriptreplay 和加速因子也非常好,但与基于控制台交换而不是进程执行的精度不同,但非常轻巧高效(目标不同,用法不同)。

最后,我认为效率更高,可读性和性能更高的是set + 1 fork,这个答案的第一个,但没关系,根据具体情况,我有时会使用strace和/或script

【讨论】:

  • Times 部分提供了很多信息,并且让我们知道分叉没什么好打喷嚏的(实际上完全控制了多种脚本)。 +1 是一个好的(如果是冗长的)答案。也许将来您应该考虑发布单独的答案
  • 非常感谢@sehe!您将在此处找到 完整的准备运行 bash 源文件:elap-bash-v3(具有允许透明使用 STDIN 等功能STDERR)
  • 在最新版本的 bash (>=4.1) 上,您可以使用 exec {BASH_XTRACEFD}&gt; 而不是 exec 3&gt;&amp;2 2&gt;,这将仅使用跟踪日志输出而不是其他 stderr 输出填充日志文件。跨度>
  • 单一日期处理方法的 exec 非常聪明,我更喜欢亚秒级精度。对于script.sh,我可以只做bash -c "exec {BASH_XTRACEFD}&gt; &gt;(tee trace.log | sed -u 's/^.*$//' | date -f - +%s.%N &gt; timing.log); set -x; . script.sh 并在不修改script.sh 的情况下获取分析数据。当不需要亚秒级精度时,我喜欢bash -c "exec {BASH_XTRACEFD}&gt;trace.log; set -x; PS4='+\t'; . script.sh,它以秒级精度为每条跟踪线打上时间戳,并且无需分叉到日期(开销低)。
【解决方案3】:

它通常有助于跟踪系统调用

strace -c -f ./script.sh

来自手册:

-c 计算每个系统调用的时间、调用和错误,并在程序退出时报告摘要。

-f 跟踪子进程 ...

这并不完全是您想要的,也不是面向行的分析器会向您显示的内容,但它通常有助于找到热点。

【讨论】:

    【解决方案4】:

    您可以查看带有 DEBUG 条件的trap 命令。有一种方法可以设置要与您的命令一起执行的命令。查看答案的注释。

    【讨论】:

    • @Dennis Williamson:我已经有一段时间没有使用它了,但是我系统上的帮助指出“如果 SIGNAL_SPEC 是 DEBUG,则在每个简单命令之后都会执行 ARG。”
    • 来自 Bash 4.0.33 help trap:“如果 SIGNAL_SPEC 是 DEBUG,ARG 会在每个简单命令之前执行。”在 Bash 3.2 中,它表示“之后”。那是一个错字。从 Bash 2.05b 开始,它之前运行过。 Reference:“本文档详细介绍了此版本 bash-2.05b-alpha1 与先前版本 bash-2.05a-release 之间的更改。... 3. Bash 中的新功能 ... w. DEBUG 陷阱现在运行 before 简单命令、((...)) 命令、[[...]] 条件命令和 for ((...)) 循环。"每个版本的测试都确认它是之前
    • @Dennis Williamson:好的,那就是我的版本。我修复了答案:)
    【解决方案5】:

    在脚本前面添加:

    N=`date +%s%N`; export PS4='+[$(((`date +%s%N`-$N)/1000000))ms][${BASH_SOURCE}:${LINENO}]: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }';
    exec 19>$HOME/logfile
    BASH_XTRACEFD=19
    set -x
    

    输出文件以毫秒为单位列出命令:

    $ tailf ~/logfile
    ++[389426ms][/home/subtleseeker/.iterm2_shell_integration.bash:96]: __bp_preexec_invoke_exec(): type -t preexec
    ++[389428ms][/home/subtleseeker/.iterm2_shell_integration.bash:113]: __bp_preexec_invoke_exec(): __bp_set_ret_value 0 /home/subtleseeker/.bashrc
    ++[389431ms][/home/subtleseeker/.iterm2_shell_integration.bash:1]: __bp_set_ret_value(): return 0
    +[389433ms][:69]: tailf /home/subtleseeker/logfile
    

    【讨论】:

    • 在什么系统上进行了测试(硬件、操作系统、包括版本等)?也许add some information to your answer? (但没有“编辑:”、“更新:”或类似的 - 答案应该看起来好像是今天写的。)
    【解决方案6】:
    Alan Hargreaves

    This post 描述了使用 DTrace 提供程序分析 Bourne shell 脚本的方法。据我所知,这适用于SolarisOpenSolaris(参见:/bin/sh DTrace Provider)。

    因此,鉴于以下 DTrace 脚本(sh_flowtime.d at GH 基于the original):

    #!/usr/sbin/dtrace -Zs
    #pragma D option quiet
    #pragma D option switchrate=10
    
    dtrace:::BEGIN
    {
            depth = 0;
            printf("%s %-20s  %-22s   %s %s\n", "C", "TIME", "FILE", "DELTA(us)", "NAME");
    }
    
    sh*:::function-entry
    {
            depth++;
            printf("%d %-20Y  %-22s %*s-> %s\n", cpu, walltimestamp,
                basename(copyinstr(arg0)), depth*2, "", copyinstr(arg1));
    }
    
    sh*:::function-return
    {
            printf("%d %-20Y  %-22s %*s<- %s\n", cpu, walltimestamp,
                basename(copyinstr(arg0)), depth*2, "", copyinstr(arg1));
            depth--;
    }
    
    sh*:::builtin-entry
    {
            printf("%d %-20Y  %-22s %*s   > %s\n", cpu, walltimestamp,
                basename(copyinstr(arg0)), depth*2, "", copyinstr(arg1));
    }
    
    sh*:::command-entry
    {
            printf("%d %-20Y  %-22s %*s   | %s\n", cpu, walltimestamp,
                basename(copyinstr(arg0)), depth*2, "", copyinstr(arg1));
    }
    

    您可以跟踪函数流,包括增量时间。

    样本输出:

    # ./sh_flowtime.d
    C TIME                  FILE                 DELTA(us)  -- NAME
    0 2007 Aug 10 18:52:51  func_abc.sh                  0   -> func_a
    0 2007 Aug 10 18:52:51  func_abc.sh                 54      > echo
    0 2007 Aug 10 18:52:52  func_abc.sh            1022880      | sleep
    0 2007 Aug 10 18:52:52  func_abc.sh                 34     -> func_b
    0 2007 Aug 10 18:52:52  func_abc.sh                 44        > echo
    0 2007 Aug 10 18:52:53  func_abc.sh            1029963        | sleep
    0 2007 Aug 10 18:52:53  func_abc.sh                 44       -> func_c
    0 2007 Aug 10 18:52:53  func_abc.sh                 43          > echo
    0 2007 Aug 10 18:52:54  func_abc.sh            1029863          | sleep
    0 2007 Aug 10 18:52:54  func_abc.sh                 33       <- func_c
    0 2007 Aug 10 18:52:54  func_abc.sh                 14     <- func_b
    0 2007 Aug 10 18:52:54  func_abc.sh                  7   <- func_a
    

    然后使用sort -nrk7 命令,您可以对输出进行排序以显示最消耗的调用。

    我不知道其他 shell 有任何可用的提供程序探测,所以做一些研究(GitHub 搜索?)或者如果你想投入一些时间,你可以基于现有的 sh 示例(参见How to activate sh DTrace Provider?):

    【讨论】:

    • 第一个 blogs.oracle.com 博客文章链接(实际上)已损坏(重定向到根目录,https://blogs.oracle.com/)。第二个blogs.oracle.com 博文链接失效:“Error 404--Not Found”
    【解决方案7】:

    Time、xtrace、bash -x、set -xset +x (2.3. Debugging Bash scripts) 仍然是调试脚本的传统方式。

    不过,为了拓宽我们的视野或对资源的使用进行更精确的控制,可以检查一些 Linux 下可用的调试和分析系统(here one of the many lists from internet):例如,Valgrind,专门用于内存调试,或sysprof 分析整个系统:

    对于系统配置:

    使用 sysprof,您可以分析您机器上运行的所有应用程序,包括多线程或多处理应用程序...

    然后,选择您感兴趣的子流程分支。


    对于 Valgrind:

    有了更多的健身房,似乎可以让 Valgrind 看到一些我们通常从二进制文件安装的程序(例如 OpenOffice)。

    如果明确要求,可以从FAQ of Valgrind 中读取 Valgrind 将分析 子进程

    ...即使默认情况下它只跟踪顶级进程, 因此,如果您的程序是由 shell 脚本、Perl 脚本或类似的东西启动的,Valgrind 将跟踪 shell、Perl 解释器或等效...

    它会在启用此选项的情况下执行此操作:

     --trace-children=yes
    

    其他参考资料:

    【讨论】:

    • 不是反对者,但这些技巧中的大多数虽然很酷,但在这里并不真正相关。在这里更欢迎提出适当的问题并自行回答 - 谷歌“stackoverflow 自我回答”以了解相关礼仪。
    • sysprof 链接已损坏:“糟糕...错误 404”。 GDB 链接可能会或可能不会半断(手动重定向 - “此页面已移至此处。”)。
    • @PeterMortensen 感谢您的编辑...链接已修复。
    猜你喜欢
    • 1970-01-01
    • 2020-11-10
    • 1970-01-01
    • 2017-08-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-23
    相关资源
    最近更新 更多