From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=74.125.82.200 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1792731738; b=QRS9J1IuNAksDbs9G6iYow1gU71AUboRFGJ4+3bQmR4shx0jDclLVeF+iBV6KONl1LYQ H3LbdtkuxHKYKa7MM9KM7NjLzDGDPX3prGpNx25P9Dxpct8RSmBBcK9Ps+w8OJAFJbPIl oSMLvtviU7x3xj9OxE0qBYFdbKITv61zl8QmZZ1WF2NGDQ5O90nQ/ANBgpndw7zGpQcW6 A5/Pyt1Cq5aJw7Whx080GO2l0o8bX/2qbW81qjyQ/PBsuilghSZqkKzIBIeVZDqOvMDJZ 958m+az6v1HPQURtpJ/RNgS3+2Y5DgGRQkB22DxsM7t4HZVGXTdYud95HYns9MJzVIg== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1792731738; h=DMARC-Filter:Received:DKIM-Signature:X-Google-DKIM-Signature: X-Forwarded-Encrypted:X-Gm-Message-State:X-Received:Date:In-Reply-To: Mime-Version:References:X-Mailer:Message-ID:Subject:From:To:Cc: Content-Type; bh=Ps3hu7XDU2mF0VAEIGd/x8ZoHr7Zk69Z1NY3Q51qspc=; b=Sno2RLw9akaxolDAiFk7+DzGMGtjgdu9N3y2Bp67a3pCOKUQEO/r8f8bfkBPG5JKn3SM Gpzj5QYNv/qpCOwKu/N70zTBi7uQf1mBBdqn3YWRC+c7Ixlva0xAIYVAiKclJeddn1nuh xcJUX+VhKaCZNwQWhtqqQkYMveeLkSRdrUWABcYL3Sfxan+HmfiPyuZtOoZphrTEUVmQr 3DQt5ImJiRR/8ASlZbupATXzefolLxcwuXXJ6q70ODbQbExCh1tD/OOohMhZ1N8CncXnc 4bEVnY1Rf3kQD7Q+qo1PLImffjjo5qLHG2qa8rLxkisE3c5S3gsUmh0yhQFVdcR2PMQ== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=google.com; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com; dkim=pass header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20251104 header.b=Ydr17v0r; arc=none smtp.remote-ip=74.125.82.200 Received: from mail-dy1-f200.google.com (mail-dy1-f200.google.com [74.125.82.200]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1C4CD606A331 for ; Fri, 23 Oct 2026 05:02:17 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org 1C4CD606A331 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20251104 header.b=Ydr17v0r Received: by mail-dy1-f200.google.com with SMTP id 5a478bee46e88-3510d0baf63so1243782eec.1 for ; Thu, 22 Oct 2026 22:02:17 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1792731737; x=1793336537; darn=vger.kernel.org; h=content-type:cc:to:from:subject:message-id:references:mime-version :in-reply-to:date:from:to:cc:subject:date:message-id:reply-to :content-type; bh=Ps3hu7XDU2mF0VAEIGd/x8ZoHr7Zk69Z1NY3Q51qspc=; b=Ydr17v0rho8fteJZGmsDBx4UG1gwj/Cko5uRxFpgLNJhnkfmdsXEY1hXDNaYMmoZyw c00zoodeSHA9989e7T+/x0OgnvOZ7o4aBHmA83WXGnv6VOdBHFqGU+wAnIruwuhHfLam 76Amk3+6BJIA0eWtyOiZOcO+qYMnIQ97JHcjDtrQcS+1DQYaKI8umVaUXRUeyns+nQAs uPnpNp97N/laS0u1Qx9IXyl9Qp3PqJz5103/4dzX7LNtb+FClqyJ4/0flIHCpMPrZLwX mQZb3UdtAAkubksRLbcW5pIQ9Z2wA75+144gbNLnLuSmB5ML+JbT2j9THrraXzVFEVS0 YTlA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792731737; x=1793336537; h=content-type:cc:to:from:subject:message-id:references:mime-version :in-reply-to:date:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to:content-type; bh=Ps3hu7XDU2mF0VAEIGd/x8ZoHr7Zk69Z1NY3Q51qspc=; b=IGmbXvR0lBd4pUkE0BHlT1wqx61AaaMF5m3fZbkmLuJeXWCrCk5wSqF0PvrULCAC+0 /4lXNcf24piP5bqVOxkpljUCpieeGvT4AUFomvjkYbRBeTUr2adW8R8eq9bOXn2YkkIe rM8ryQdmlBYRS8+TtJFcUGpqeXFDghgNuucE2gX2/oo7N8jNF4wZ635Oq60/9HXR0Fz6 +U9Iiwt8gbd93cBcl5Ax0PSzbo0cq6QalvV7NrHqKGTC0O5RhWCSu5QBRPQPyhPHoCV4 ORXuuPb4Q5uRZOV95P+UOT1G2EGSd5k/z9sSGL5G9MOPreuAOYQAi+S52d2A+61/DDh9 tSlQ== X-Forwarded-Encrypted: i=1; AKwUvByNP5AmJWK2puxhUJBg2eGekn+5VxZnOaGtBAwM9oFeMG/1zgP8ynCrty+DM+1NhFH9p9L9/+UAAfJ8BBU=@vger.kernel.org X-Gm-Message-State: AFq9FYL1QPKoIR003m27uB8y7v90xcN2k6fIdxkbFZ3sHptZQrsSFb5o 44frojeswdUA67O0NLwk1yl8tn/6FdlXmfcfje9AxgRavjFyXiz3Ggw+v8cwmHfklwRx7xqrWZ3 YbfN/Cp3aKw== X-Received: from dybta14.prod.google.com ([2002:a05:7301:6f0e:b0:368:2c6d:de10]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a05:7301:4395:b0:368:6f29:f26c with SMTP id 5a478bee46e88-369d916e13emr1607534eec.4.1792731736840; Thu, 22 Oct 2026 22:02:16 -0700 (PDT) Date: Thu, 22 Oct 2026 22:02:06 -0700 In-Reply-To: <20261023050207.2449976-1-irogers@google.com> X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list Mime-Version: 1.0 References: <20261023041819.2361900-1-irogers@google.com> <20261023050207.2449976-1-irogers@google.com> X-Mailer: git-send-email 2.56.0.307.g5fe7bd680c-goog Message-ID: <20261023050207.2449976-3-irogers@google.com> Subject: [PATCH v4 2/2] perf test: Test perf timechart --live From: Ian Rogers To: irogers@google.com, acme@kernel.org, alice.mei.rogers@gmail.com, namhyung@kernel.org Cc: adrian.hunter@intel.com, atrajeev@linux.ibm.com, changbin.du@huawei.com, dapeng1.mi@linux.intel.com, james.clark@linaro.org, jolsa@kernel.org, linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org, mingo@redhat.com, peterz@infradead.org, tanze@kylinos.cn, thomas.falcon@intel.com, wutengda@huaweicloud.com Content-Type: text/plain; charset="UTF-8" From: Alice Rogers Extend test_ttimechart_python.sh to cover --live option validation, LiveRecorder's perf record subprocess, control pipe and -o output file, a synthetic live UI test exercising the rolling window, pruning, pause/resume and zoom following, and a live workload recording test when scheduler tracepoints are accessible. Signed-off-by: Alice Rogers Co-developed-by: Ian Rogers Signed-off-by: Ian Rogers --- .../tests/shell/test_ttimechart_python.sh | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) diff --git a/tools/perf/tests/shell/test_ttimechart_python.sh b/tools/perf/tests/shell/test_ttimechart_python.sh index 87b91ed8fc16..733e9edfd976 100755 --- a/tools/perf/tests/shell/test_ttimechart_python.sh +++ b/tools/perf/tests/shell/test_ttimechart_python.sh @@ -82,6 +82,34 @@ test_plumbing() { err=1 return fi + if perf timechart --live -i "${perfdata}" > "${output}" 2>&1 || + ! grep -q -- "--live can't be used with -i" "${output}"; then + echo "perf timechart --tui plumbing test [Failed: --live with -i not rejected]" + cat "${output}" + err=1 + return + fi + if perf timechart --live --window 0 < /dev/null > "${output}" 2>&1 || + ! grep -q -- "--window must be a positive" "${output}"; then + echo "perf timechart --tui plumbing test [Failed: --window 0 not rejected]" + cat "${output}" + err=1 + return + fi + if perf timechart --tui --window 5 < /dev/null > "${output}" 2>&1 || + ! grep -q -- "require --live" "${output}"; then + echo "perf timechart --tui plumbing test [Failed: --window without --live]" + cat "${output}" + err=1 + return + fi + if perf timechart --live -I -P < /dev/null > "${output}" 2>&1 || + ! grep -q -- "-I cannot be used with -P or -T" "${output}"; then + echo "perf timechart --tui plumbing test [Failed: -I with -P not rejected]" + cat "${output}" + err=1 + return + fi echo "perf timechart --tui plumbing test [Success]" } @@ -115,6 +143,43 @@ test_no_events() { err=1 return fi + # Exercise LiveRecorder's perf record subprocess, control pipe and + # -o output file using task-clock when tracepoints may be unavailable. + rm -f "${clockdata}" + if ! "$PYTHON" - "${PERF_BIN:-perf}" "${clockdata}" \ + "$(declare -f workload); workload" > "${output}" 2>&1 <<'EOF' +import asyncio +import os +import sys +import ttimechart as tc + +async def run() -> None: + rec = tc.LiveRecorder(sys.argv[1], False, False, ["bash", "-c", sys.argv[3]], sys.argv[2]) + rec.events = lambda: ["task-clock"] + rec.start() + app = tc.TimechartApp("live", False, False, [], live=rec, window=5.0) + try: + async with app.run_test(size=(120, 40)) as pilot: + await pilot.press("space") + await pilot.pause(0.1) + await pilot.press("space") + for _ in range(300): + if app.return_code is not None: + break + await pilot.pause(0.05) + finally: + rec.stop() + if app.return_code != 1 or not app.data.nr_samples or not os.path.getsize(sys.argv[2]): + raise RuntimeError(f"Unexpected result: rc={app.return_code} samples={app.data.nr_samples}") + +asyncio.run(run()) +EOF + then + echo "ttimechart no events test [Failed: LiveRecorder task-clock test]" + cat "${output}" + err=1 + return + fi echo "ttimechart no events test [Success]" } @@ -218,8 +283,245 @@ EOF echo "ttimechart headless UI test [Success]" } +test_live_synthetic() { + echo "ttimechart live UI test (synthetic)" + # Exercise live mode's rolling window, pruning, pause/resume and + # following with a fake recorder, which doesn't need tracepoint + # permissions. + if ! "$PYTHON" - > "${output}" 2>&1 <<'EOF' +import asyncio +import threading +import time +from time import monotonic_ns +import ttimechart as tc + +SEC = 1_000_000_000 + +class Ev: + def __init__(self, name): + self.name = name + def __str__(self): + return f"evsel({self.name})" + +class Sample: + def __init__(self, name, t, cpu, tid, **kw): + self.evsel = Ev(name) + self.sample_time = t + self.sample_cpu = cpu + self.sample_tid = tid + self.sample_pid = tid + self.sample_id = hash(name) + for k, v in kw.items(): + setattr(self, k, v) + +class FakeRecorder: + def __init__(self): + self.origin = monotonic_ns() + self.data_fd = -1 + self.flush_interval = 0.05 + self.paused = False + self.pause_calls = [] + self.stopped = threading.Event() + self.finish = False + + def backlog(self): + return 0.0 + + def set_paused(self, paused): + self.paused = paused + self.pause_calls.append(paused) + + def stop(self): + self.stopped.set() + + def error_text(self): + return "" + +async def run() -> None: + rec = FakeRecorder() + + def fake_read(data, _fd): + t0 = rec.origin + # Task 300 only runs at the start so pruning should drop it once the + # 1s window advances past t0 + 3000. + data.process_event(Sample("sched:sched_wakeup", t0 + 1000, 0, 1, + pid=300, comm="early", common_pid=1, common_flags=0)) + data.process_event(Sample("sched:sched_switch", t0 + 2000, 0, 0, + prev_pid=0, prev_comm="swapper", prev_state=1, + next_pid=300, next_comm="early")) + data.process_event(Sample("sched:sched_switch", t0 + 3000, 0, 300, + prev_pid=300, prev_comm="early", prev_state=1, + next_pid=0, next_comm="swapper")) + cur = 0 + while not rec.stopped.is_set() and not rec.finish: + now = monotonic_ns() + if rec.paused: + time.sleep(0.01) + continue + nxt = 100 if cur == 0 else 0 + comm = "worker" if nxt == 100 else "swapper" + prev_comm = "worker" if cur == 100 else "swapper" + if nxt: + data.process_event(Sample("sched:sched_wakeup", now, 0, cur or 1, + pid=nxt, comm=comm, common_pid=cur or 1, + common_flags=0)) + data.process_event(Sample("sched:sched_switch", now, 0, cur, + prev_pid=cur, prev_comm=prev_comm, prev_state=1, + next_pid=nxt, next_comm=comm)) + cur = nxt + time.sleep(0.005) + + tc.read_events = fake_read + app = tc.TimechartApp("live", False, False, [], live=rec, window=1.0) + try: + async with app.run_test(size=(120, 40)) as pilot: + for _ in range(300): + with app.data.lock: + if app.data.pruned_to > rec.origin + 3000: + break + await pilot.pause(0.05) + with app.data.lock: + if app.data.pruned_to <= rec.origin + 3000 or 300 in app.data.tasks: + raise RuntimeError("Task outside the window was not pruned") + starts = [t.segs.starts[0] for t in app.data.tasks.values() if len(t.segs)] + if not starts or min(starts) < app.data.pruned_to: + raise RuntimeError(f"Unpruned segments: {starts} < {app.data.pruned_to}") + if app.window.last - app.window.first != SEC: + raise RuntimeError(f"Unexpected window span: {app.window}") + + await pilot.press("space") + await pilot.pause(0.2) + if rec.pause_calls != [True] or "Paused" not in app.sub_title: + raise RuntimeError(f"Did not pause: {rec.pause_calls} {app.sub_title}") + frozen = app.window.last + await pilot.pause(0.4) + if app.window.last != frozen: + raise RuntimeError("Window moved while paused") + await pilot.press("space") + await pilot.pause(0.3) + if rec.pause_calls != [True, False] or not app.following: + raise RuntimeError("Did not resume") + + await pilot.press("plus") + await pilot.pause(0.2) + if app.following: + raise RuntimeError("Zooming did not stop following") + await pilot.press("0") + await pilot.pause(0.2) + if not app.following: + raise RuntimeError("Resetting zoom did not resume following") + + rec.finish = True + for _ in range(100): + if not app.recording: + break + await pilot.pause(0.05) + if app.recording or "Finished" not in app.sub_title: + raise RuntimeError(f"Did not finish: {app.sub_title}") + finally: + rec.stop() + + # Also test live I/O-only mode (-I). + io_events = tc.LiveRecorder("perf", False, False, [], None, io_only=True).events() + if not io_events or not all(e.startswith("syscalls:sys_") for e in io_events): + raise RuntimeError(f"Unexpected I/O events: {io_events}") + io_rec = FakeRecorder() + + def fake_io_read(data, _fd): + now = monotonic_ns() + # Self I/O should be ignored in live mode. + own = tc.os.getpid() + data.process_event(Sample("syscalls:sys_enter_read", now, 0, own, fd=3)) + data.process_event(Sample("syscalls:sys_exit_read", now + 1000, 0, own, ret=64)) + # Workload I/O should be recorded. + data.process_event(Sample("syscalls:sys_enter_read", now + 2000, 0, 4242, fd=4)) + data.process_event(Sample("syscalls:sys_exit_read", now + 5000, 0, 4242, ret=4096)) + data.process_event(Sample("syscalls:sys_enter_write", now + 6000, 0, 4242, fd=5)) + data.process_event(Sample("syscalls:sys_exit_write", now + 9000, 0, 4242, ret=2048)) + + tc.read_events = fake_io_read + io_app = tc.TimechartApp("live", False, False, [], live=io_rec, window=1.0, io_only=True) + async with io_app.run_test(size=(120, 40)) as pilot: + for _ in range(100): + if not io_app.recording: + break + await pilot.pause(0.05) + await pilot.pause() + tabs = io_app.query_one(tc.TabbedContent) + if tabs.active != "io" or not io_app.io_tasks or io_app.data.io_events != 4: + raise RuntimeError(f"Unexpected I/O state: active={tabs.active} " + f"tasks={len(io_app.io_tasks)} events={io_app.data.io_events}") + tabs.active = "summary" + await pilot.pause() + await pilot.press("enter") + await pilot.pause() + if tabs.active != "io": + raise RuntimeError(f"Summary enter did not activate I/O tab: {tabs.active}") + print("live synthetic ok") + +asyncio.run(run()) +EOF + then + echo "ttimechart live UI test (synthetic) [Failed: exception]" + cat "${output}" + err=1 + return + fi + if ! grep -q "live synthetic ok" "${output}"; then + echo "ttimechart live UI test (synthetic) [Failed: unexpected output]" + cat "${output}" + err=1 + return + fi + echo "ttimechart live UI test (synthetic) [Success]" +} + +test_live_record() { + echo "ttimechart live record test" + rm -f "${perfdata}.live" + if ! "$PYTHON" - "${PERF_BIN:-perf}" "${perfdata}.live" \ + "$(declare -f workload); workload" > "${output}" 2>&1 <<'EOF' +import asyncio +import sys +import ttimechart as tc + +async def run() -> None: + rec = tc.LiveRecorder(sys.argv[1], False, False, ["bash", "-c", sys.argv[3]], sys.argv[2]) + rec.start() + app = tc.TimechartApp("live", False, False, [], live=rec, window=5.0) + try: + async with app.run_test(size=(120, 40)) as pilot: + for _ in range(600): + if not app.recording: + break + await pilot.pause(0.1) + if app.recording or not app.data.has_events(): + raise RuntimeError("Live recording of workload did not complete") + print("live record ok") + finally: + rec.stop() + +asyncio.run(run()) +EOF + then + echo "ttimechart live record test [Failed: exception]" + cat "${output}" + err=1 + return + fi + if ! perf script ttimechart -i "${perfdata}.live" --dump > "${output}" 2>&1 || + ! grep -q "^Duration: .*sched events: [1-9]" "${output}"; then + echo "ttimechart live record test [Failed: saved perf.data unreadable]" + cat "${output}" + err=1 + return + fi + echo "ttimechart live record test [Success]" +} + test_plumbing test_no_events +test_live_synthetic if ! record; then echo "Skipping remaining tests, failed to record scheduler events (permissions?)" @@ -232,6 +534,7 @@ fi test_dump test_headless_ui +test_live_record cleanup exit $err -- 2.56.0.307.g5fe7bd680c-goog