
whytime은 리눅스에서 명령어를 실행하거나 이미 실행 중인 프로세스에 붙어, 전체 실행 시간(wall-clock time)이 CPU 작업·네트워크 대기·파일 동기화·잠자기 등에 어떻게 나뉘는지 보여주는 도구다. eBPF로 프로세스 트리를 추적하고 핵심 경로를 따라 시간을 분류한다. 지원 대상은 BTF가 활성화된 Linux 5.18 이상, x86_64 또는 aarch64 시스템이다.
time이 명령어의 실제·사용자·시스템 시간을 알려준다면 whytime은 실제 시간의 나머지가 어디에 쓰였는지를 보여준다. CPU 사용량만 표본으로 보는 perf record나 perf top과 달리 CPU 밖에서 기다린 시간도 다룬다. 각 보고서의 분류는 핵심 경로를 따라 전체 관찰 구간을 빠짐없이 나누며, 항목의 비율을 합하면 100%가 된다. 따라서 어떤 대기나 작업을 줄여야 명령어가 더 빨리 끝날지 파악하는 데 쓰도록 설계됐다.
예를 들어 sleep, 파일 쓰기와 fsync, 네트워크 다운로드를 연달아 수행한 사례에서는 실행 시간 중 47%가 speed.cloudflare.com:443 네트워크 대기, 34%가 ~/.cache의 fsync, 13%가 타이머 대기, 4%가 curl의 CPU 작업으로 표시됐다. xz -T4 사례에서는 CPU 시간이 실제 시간보다 길어 평균 약 3개의 CPU가 사용된 것으로 나타났다. 주 스레드가 작업자 스레드를 기다리는 경우에도 whytime은 기다림을 끝낸 작업자까지 추적해 시간을 귀속한다.
설치와 실행 조건
whytime은 x86_64와 aarch64용 정적 바이너리를 제공하며, 설치 방식에 관계없이 같은 프로그램을 설치한다. 실행에는 관리자 권한 또는 CAP_BPF와 CAP_PERFMON 권한이 필요하다. 설치 과정만으로 권한이 부여되지는 않는다. 명령 앞에 sudo를 붙여 실행하거나 바이너리에 기능 권한을 설정할 수 있다. 커널 스택을 불꽃 그래프로 표시하려면 CAP_SYSLOG 또는 root 권한도 필요하다.
Linux 5.18 이상이어야 하며 /sys/kernel/btf/vmlinux에 BTF 정보가 있어야 한다. x86_64 또는 aarch64가 지원된다. Linux 5.18은 sched_switch가 이전 작업의 상태를 전달하기 시작한 버전으로, whytime은 이 정보를 이용해 차단 상태와 선점 상태를 구분한다. whytime doctor는 커널, BTF, 권한 등 실행 조건을 점검하고 누락된 항목의 수정 방법을 출력한다.
릴리스에는 두 아키텍처용 패키지가 제공된다. Debian·Ubuntu용 .deb는 sudo apt install ./whytime_*.deb, Fedora·RHEL용 .rpm은 sudo dnf install ./whytime-*.rpm으로 설치한다. openSUSE는 sudo zypper install --allow-unsigned-rpm ./whytime-*.rpm, Alpine은 sudo apk add --allow-untrusted ./whytime_*.apk, Arch는 sudo pacman -U ./whytime-*.pkg.tar.zst를 사용한다. Arch의 AUR에는 릴리스 바이너리인 whytime-bin과 릴리스 태그에서 빌드하는 whytime이 있다. Nix flakes에서는 다음과 같이 실행하거나 프로필에 설치할 수 있다.
nix run github:Arinerron/whytime -- doctor
nix profile add github:Arinerron/whytime # Nix 2.30 이전 버전: nix profile install
설치 스크립트는 최신 릴리스를 내려받아 SHA-256을 확인한 뒤 바이너리와 매뉴얼, 셸 자동 완성 파일을 설치한다. 기본 설치 위치는 /usr/local이며, sudo 없이 실행하면 ~/.local을 사용한다. 같은 스크립트를 다시 실행하면 업그레이드한다.
curl -fsSL https://github.com/Arinerron/whytime/releases/latest/download/install.sh | sh
특정 릴리스나 설치 경로를 지정하려면 WHYTIME_VERSION=v0.1.1 또는 --prefix DIR을 전달한다. 릴리스 tarball을 직접 설치할 때는 최신 태그를 확인하고 아키텍처에 맞는 파일과 체크섬을 내려받아 검증한 뒤 압축을 풀면 된다.
tag=$(curl -fsSLo /dev/null -w '%{url_effective}' https://github.com/Arinerron/whytime/releases/latest)
tag=${tag##*/} # e.g. v0.1.1
name=whytime-$tag-$(uname -m)-linux-musl # x86_64 or aarch64
curl -fsSLO "https://github.com/Arinerron/whytime/releases/download/$tag/$name.tar.gz"
curl -fsSLO "https://github.com/Arinerron/whytime/releases/download/$tag/$name.tar.gz.sha256"
sha256sum -c "$name.tar.gz.sha256"
tar -xzf "$name.tar.gz"
sudo install -m 755 "$name/whytime" /usr/local/bin/whytime
sudo install -m 644 "$name"/man/*.1 /usr/local/share/man/man1/
sudo install -m 644 "$name/completions/whytime.bash" /usr/share/bash-completion/completions/whytime
sudo install -m 644 "$name/completions/_whytime" /usr/share/zsh/site-functions/_whytime
install -Dm 644 "$name/completions/whytime.fish" ~/.config/fish/completions/whytime.fish
# also completions/whytime.elv (elvish) and completions/_whytime.ps1 (PowerShell)
Cargo로 설치하면 로컬에서 whytime과 BPF 객체를 빌드한다. 이 방식에는 BPF 대상을 포함한 clang과 Rust 1.94 이상이 필요하다. --locked는 whytime 릴리스 당시의 의존성 버전을 사용하며, 결과물은 ~/.cargo/bin에 설치되는 동적 링크 glibc 바이너리다. cargo binstall은 0.1.2 이후 정적 릴리스 바이너리를 설치한다.
cargo install --locked whytime # 소스에서 빌드
cargo binstall whytime # 정적 릴리스 바이너리(0.1.2 이후)
컨테이너 이미지는 Alpine 기반 ghcr.io/arinerron/whytime이다. 커널이 호스트 PID를 보고하므로 --pid=host가 필요하다. 호스트 프로세스를 관찰하는 예시는 다음과 같다.
docker run --rm --pid=host --cap-add BPF --cap-add PERFMON --cap-add SYS_PTRACE \\
--cap-add SYSLOG ghcr.io/arinerron/whytime -p PID
-p PID는 호스트의 프로세스를 프로파일링하고, -- COMMAND는 컨테이너 안에서 명령을 실행해 추적한다. doctor는 호스트 환경을 점검한다.
명령어 실행과 보고서 확인
명령어를 감싸려면 whytime -- 뒤에 실행할 명령을 적는다. 파이프라인이나 셸 스크립트도 셸 명령으로 감쌀 수 있다.
sudo whytime -- make -j8
sudo whytime -- sh -c 'cmd1 | cmd2' # 파이프라인과 스크립트는 셸로 감싼다
관찰 구간은 명령어의 실행부터 마지막 스레드가 끝날 때까지다. 명령어가 시작한 프로세스도 추적하지만, 명령어가 남긴 백그라운드 프로세스가 끝날 때까지 기다리지는 않는다. 프로그램 자체의 출력은 그대로 전달되고 whytime 보고서는 표준 오류(stderr)에 출력된다. whytime은 실행한 명령의 종료 코드를 반환한다. 명령이 신호 N에 의해 종료되면 종료 상태는 128 + N이다. 추적을 시작하지 못하거나 출력 파일을 만들지 못하면 125, 실행 파일이 아니면 126, 명령을 찾지 못하면 127을 반환한다. Ctrl-C는 whytime을 사용하지 않을 때처럼 명령에 전달되며, 명령이 끝나면 whytime은 보고서를 출력한다.
이미 실행 중인 프로세스에는 -p PID로 붙는다. 대상과 그 자손을 관찰하며 대상에는 신호를 보내지 않는다. 관찰은 연결한 시점부터 시작하므로 그 전의 시간은 포함하지 않는다. 연결 시점에 이미 존재하던 스레드의 상태와 대기 대상은 /proc에서 읽는다. 연결할 수 없으면 종료 코드 1, 추적을 시작할 수 없으면 125를 반환한다. 대상이 없는 프로세스, 스레드 ID, 좀비, PID 1, whytime 자체 또는 권한이 없는 다른 사용자의 프로세스에는 연결할 수 없다.
--duration D는 지정한 시간이 지나면 관찰을 멈추고 보고서를 출력한다. 이때 명령이나 대상 프로세스는 계속 실행된다. 명령 실행 모드에서는 이후에도 신호를 전달하며, 명령이 끝나면 명령의 종료 코드를 반환한다. JSON의 ended_by에는 관찰 구간이 exit, duration, 또는 연결 모드의 interrupt 중 어떤 이유로 끝났는지 기록된다. 시간은 500ms, 30s, 1.5s, 2m, 1h처럼 쓰며, 단위가 없는 숫자는 초로 해석한다.
주요 옵션은 --json(텍스트 대신 whytime.report/1 JSON 출력), --timeline(관찰 구간을 같은 길이의 10개 구간으로 나눠 각 구간의 주요 항목 표시), --perfetto FILE(스레드별 타임라인과 핵심 경로를 Chrome Trace Event JSON으로 저장), --flamegraph FILE(핵심 경로의 벽시계 시간 불꽃 그래프를 SVG로 저장), --folded FILE(같은 스택을 마이크로초 단위 접힌 형식으로 저장), --record FILE(추적 파일도 저장), -p 또는 --pid PID(실행 중인 프로세스에 연결)이다. 옵션은 명령보다 앞에 둔다. 명령이 -로 시작하지 않는다면 --는 생략할 수 있다. -h와 --help, -V와 --version은 어디서나 사용할 수 있고, 알 수 없는 옵션은 오류다. WHYTIME_LOG=debug를 설정하면 BPF 검증기 로그를 포함한 디버그 기록을 출력한다.
시간 분류와 추가 시각화
일반 보고서는 전체 시간, 프로세스 트리 전체의 CPU 시간, 관찰한 프로세스 수와 병렬 처리 비율을 표시한다. 주요 분류는 비율이 큰 순서로 최대 8개까지 나오며, 1% 미만 항목과 설명되지 않은 시간은 other에 포함된다. 보고서에는 커널이 이벤트를 잃었는지, 실행별 주의사항도 표시된다.
분류에는 CPU에서 실행된 시간, 자손 프로세스가 계산하는 동안 기다린 시간, 실행 가능한 상태지만 CPU를 기다린 시간, 타이머 대기, fsync와 파일 I/O, 파일 잠금, 비동기 I/O 완료, 네트워크 응답 대기, 파이프·소켓 등의 IPC, futex, poll/epoll, 신호 대기, 터미널·장치 I/O, 페이지 폴트, 기타 대기가 포함된다. 파일 관련 항목은 디렉터리 단위로 표시하며, 네트워크는 가능한 경우 호스트 이름과 포트를 표시한다.
추적 중인 다른 스레드나 프로세스가 기다림을 끝낸 경우에는 기다린 시간 자체 대신 그 작업이 그동안 수행한 일을 핵심 경로에 반영한다. 따라서 자식 프로세스를 기다린 시간이나 추적 대상 스레드가 깨운 futex·파이프 대기가 별도의 대기 항목으로 남지 않을 수 있다. poll·select·epoll은 준비 상태가 된 파일 기술자에 따라 분류하므로, 예를 들어 curl은 poll 대신 서버에 대한 네트워크 대기로 표시될 수 있다.
--timeline은 실행 구간을 10개로 나누고 각 구간에서 큰 항목을 보여준다. --perfetto FILE로 만든 파일은 ui.perfetto.dev 또는 chrome://tracing에서 열 수 있다. 추적된 스레드마다 CPU 실행, 실행 대기, 차단 구간과 fork·exec·exit 표시가 제공되며, 보고서에 합산되는 구간을 별도의 핵심 경로 트랙에서 볼 수 있다.
--flamegraph FILE은 핵심 경로의 벽시계 시간을 폭으로 나타내는 독립 실행형 SVG를 만든다. --folded FILE은 같은 스택을 flamegraph.pl, inferno 또는 speedscope에서 쓸 수 있는 형식으로 저장한다. 스택은 보고서 분류와 프로세스 이름, 호출 스택 순으로 구성된다. CPU에서 실행된 스택은 199Hz 표본 수집을 사용하고, 차단 시간의 스택은 차단 시점의 스택을 사용한다. 사용자 스택은 프레임 포인터를 통해 해석하며 DWARF 정보나 인라인 프레임은 사용하지 않는다. 프레임 포인터가 없는 프로그램은 일부 호출자를 표시하지 못할 수 있다. 불꽃 그래프 생성은 일반 실행보다 부담이 크며, 추적 스레드가 차단될 때의 스택 수집과 표본 수집을 추가한다.
--json은 표준 오류에 JSON 객체 하나를 출력한다. 보고서 스키마는 whytime.report/1이며 벽시계 시간, 종료 이유와 코드, 프로세스별 CPU 시간, 분류 항목, 추적 품질과 주의사항을 담는다. --timeline은 timeline, 연결 모드는 attached_pid를 추가한다. 네트워크 항목에는 연결 수와 주소가, 실행 대기 항목에는 그동안 CPU를 점유한 작업이 포함될 수 있다. /1에서는 선택 필드와 분류 값 등을 추가할 수 있지만 필드 제거·이름 변경·자료형 변경·의미 변경은 허용하지 않으며, 그런 변경에는 /2가 필요하다. JSON 소비자는 모르는 필드를 무시하고, 알 수 없는 분류를 other로 처리해야 한다.
--record FILE은 커널 기록과 보고서에 사용된 조회 정보를 저장한다. 나중에 whytime report FILE로 관리자 권한이나 원래 프로세스 없이 보고서를 다시 만들 수 있으며, 같은 입력으로 보고서를 바이트 단위까지 재현한다. 모든 보고서 옵션을 재생에 사용할 수 있지만, 불꽃 그래프 옵션은 해당 스택과 기호를 함께 기록한 파일이 필요하다. 기록 파일 형식이 호환되지 않거나 파일이 손상·잘림 상태이면 재생은 종료 코드 2를 반환한다. 추적 파일에는 명령줄, 경로, 호스트 이름이 들어갈 수 있으므로 공유 전에는 보안 안내를 확인해야 한다.
한계와 성능 비용
보고서에는 핵심 경로 하나만 포함된다. 핵심 경로와 병렬로 실행된 다른 작업자들의 작업은 분류 항목에 포함되지 않지만, 전체 CPU 사용량은 CPU 시간과 프로세스별 CPU 시간에 표시된다. 기다림을 깨운 작업이 실제로 기다리던 자원이었다고 가정해 추적하므로, futex를 깨운 스레드가 반드시 필요한 작업을 수행한 경우가 아닐 수 있다. 시간 초과로 끝난 대기는 시간만으로 이후의 깨움과 연결된다.
추적 대상 트리 밖의 데몬, 데이터베이스, 원격 서비스, X 서버는 대기 대상 자원으로만 나타난다. 호스트 이름은 관찰 구간에 추적 대상이 DNS(포트 53)나 systemd-resolved를 통해 조회한 경우에만 알 수 있다. /etc/hosts, 캐시, DNS-over-TLS·HTTPS 또는 연결 전의 조회에서는 ip:port로 표시될 수 있다. 연결 모드는 연결 전의 시간을 포착하지 못한다. 파일 경로는 시스템 호출이 처음 차단된 시점에 차단된 작업의 마운트 네임스페이스를 기준으로 수집하며, 256바이트 또는 경로 구성 요소 32개를 넘으면 잘린다.
eBPF 시스템 호출 추적은 whytime 실행 중 시스템의 모든 작업을 시스템 호출의 느린 경로로 보내며, 시스템 호출당 약 100ns의 비용이 발생한다. 1바이트 읽기처럼 시스템 호출이 많은 마이크로벤치마크는 추적 중 CPU 사용량이 1.4~1.8배가 될 수 있다. 시스템 호출이 적은 명령은 비용도 비례해 작아진다. 불꽃 그래프는 스택 수집 비용을 더하며, 컨텍스트 전환이 많은 작업에서 그 영향이 나타난다.
일부 추적 기능은 선택 사항이다. 시스템 호출 추적 지점이 없으면 차단 시간 일부가 other로 분류될 수 있고, 준비 상태·페이지 폴트 기능이 없거나 DNS 조회를 포착하지 못하면 각각 해당 분류가 제한되거나 네트워크 대상이 ip:port로 남는다. 불꽃 그래프와 접힌 스택에는 kernel.perf_event_max_stack이 최소 127이어야 하며, 기본값은 127이다. 커널 버전과 BTF 조건을 충족하는 배포판 가운데 Arch Linux와 Ubuntu 메인라인 6.12 CI 환경만 테스트 스위트 실행 대상으로 명시돼 있다. Debian 12·13, Ubuntu 24.04 및 HWE 커널을 사용하는 22.04, Fedora, openSUSE Tumbleweed 등은 조건을 충족하면 작동할 것으로 안내한다. Ubuntu 22.04 GA 커널 5.15와 RHEL 9 계열 5.14는 최소 버전 미달로 거부된다.
소스 빌드에는 BPF 대상이 포함된 clang과 rustup을 통한 Rust stable이 필요하다. 저장소를 재귀적으로 복제하지 않았다면 git submodule update --init으로 tracekit 하위 모듈을 가져와야 한다. 저장소의 기본 빌드는 musl 기반 정적 바이너리이며 aarch64용 빌드도 제공한다. 프로젝트는 MIT 라이선스를 사용하고, 릴리스 tarball에는 제3자 라이선스 문서가 포함된다. 설치·사용법과 제한의 전체 내용은 whytime 저장소와 개발자 안내서에 정리돼 있다.
부록: 원문 예시
$ sudo whytime -- sh -c 'sleep 0.5; dd if=/dev/zero of=$HOME/.cache/wt-demo bs=4k count=200 oflag=dsync 2>/dev/null; curl -s -o /dev/null https://speed.cloudflare.com/__down?bytes=20000000' wall 3.83s cpu 194ms across 3 processes · 0.05× parallel 47% blocked on network: speed.cloudflare.com:443 (1 connection) 34% fsync on ~/.cache 13% timer sleep (sleep) 4% on-CPU (curl) 2% other critical path: 3 processes · 0 events lost
원문에 실린 예시 2
$ sh -c 'while true; do sleep 0.2; dd if=/dev/zero of=/dev/null bs=1M count=3000 2>/dev/null; done' & $ sudo whytime -p $! --duration 3s --record demo.wt wall 3.00s cpu 1.54s across 17 processes · 0.51× parallel 51% waiting on child: dd … count=3000 48% timer sleep (sleep) 1% other critical path: 17 processes · 0 events lost note: attached to running pid 2814784: time before attach is not covered, and each pre-existing thread's first interval is classified from /proc note: --duration elapsed; the command was still running
원문에 실린 예시 3
$ sudo whytime --timeline --perfetto t.json -- sh -c 'sleep 0.5; dd if=/dev/zero of=$HOME/.cache/wt-demo bs=4k count=200 oflag=dsync 2>/dev/null; curl -s -o /dev/null https://speed.cloudflare.com/__down?bytes=20000000' wall 2.74s cpu 145ms across 3 processes · 0.05× parallel 48% fsync on ~/.cache 27% blocked on network: speed.cloudflare.com:443 (1 connection) 18% timer sleep (sleep) 4% on-CPU (curl) 1% waiting on child: dd 2% other timeline (274ms slices): 0.00–0.27s sleep sleep 98% · cpu sh 1% 0.27–0.55s sleep sleep 85% · fsync ~/.cache 14% 0.55–0.82s fsync ~/.cache 97% · child dd 3% 0.82–1.09s fsync ~/.cache 97% · child dd 3% 1.09–1.37s fsync ~/.cache 97% · child dd 2% 1.37–1.64s fsync ~/.cache 97% · child dd 3% 1.64–1.92s fsync ~/.cache 80% · ipc woken by systemd-resolve 7% · net speed.cloudflare.com:443 4% 1.92–2.19s net speed.cloudflare.com:443 89% · cpu curl 11% 2.19–2.46s net speed.cloudflare.com:443 87% · cpu curl 12% · runqueue curl 1% 2.46–2.74s net speed.cloudflare.com:443 89% · cpu curl 10% · runqueue curl 1% critical path: 3 processes · 0 events lost
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요