From: Ian Rogers <irogers@google.com>
To: Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Namhyung Kim <namhyung@kernel.org>, Jiri Olsa <jolsa@kernel.org>,
Ian Rogers <irogers@google.com>,
Adrian Hunter <adrian.hunter@intel.com>,
James Clark <james.clark@linaro.org>,
Thomas Falcon <thomas.falcon@intel.com>,
Alice Rogers <alice.mei.rogers@gmail.com>,
Changbin Du <changbin.du@huawei.com>,
Tengda Wu <wutengda@huaweicloud.com>, tanze <tanze@kylinos.cn>,
Athira Rajeev <atrajeev@linux.ibm.com>,
Dapeng Mi <dapeng1.mi@linux.intel.com>,
linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org
Subject: [PATCH v1 13/13] perf test: Test perf timechart --live
Date: Fri, 2 Oct 2026 11:26:22 -0700 [thread overview]
Message-ID: <20261002182624.3259797-14-irogers@google.com> (raw)
In-Reply-To: <20261002182624.3259797-1-irogers@google.com>
From: Alice Rogers <alice.mei.rogers@gmail.com>
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.
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Alice Rogers <alice.mei.rogers@gmail.com>
Co-developed-by: Ian Rogers <irogers@google.com>
Signed-off-by: Ian Rogers <irogers@google.com>
---
.../tests/shell/test_ttimechart_python.sh | 300 ++++++++++++++++++
1 file changed, 300 insertions(+)
diff --git a/tools/perf/tests/shell/test_ttimechart_python.sh b/tools/perf/tests/shell/test_ttimechart_python.sh
index 87b91ed8fc16..495060586cfc 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,242 @@ 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)
+ 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}")
+
+ # 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 +531,7 @@ fi
test_dump
test_headless_ui
+test_live_record
cleanup
exit $err
--
2.56.0.rc1.315.gc6ed9934b7-goog
prev parent reply other threads:[~2026-10-02 18:27 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 18:26 [PATCH v1 00/13] perf timechart/list/treport: Interactive Textual TUIs and perf python session improvements Ian Rogers
2026-10-02 18:26 ` [PATCH v1 01/13] perf session: Don't flush remaining events once processing is done Ian Rogers
2026-10-02 18:26 ` [PATCH v1 02/13] perf python: Quietly stop processing events when a callback raises Ian Rogers
2026-10-02 18:26 ` [PATCH v1 03/13] perf python: Lazily copy events and samples from process_events Ian Rogers
2026-10-02 18:26 ` [PATCH v1 04/13] perf python: Lazily resolve sample callchains Ian Rogers
2026-10-02 18:26 ` [PATCH v1 05/13] perf python: Release the GIL while processing session events Ian Rogers
2026-10-02 18:26 ` [PATCH v1 06/13] perf list: Add a --tui option to launch ilist Ian Rogers
2026-10-02 18:26 ` [PATCH v1 07/13] perf test: Add a test for the ilist script Ian Rogers
2026-10-02 18:26 ` [PATCH v1 08/13] perf treport: Show the profile while it loads Ian Rogers
2026-10-02 18:26 ` [PATCH v1 09/13] perf test: Add a test for the treport script Ian Rogers
2026-10-02 18:26 ` [PATCH v1 10/13] perf timechart: Add an interactive --tui mode Ian Rogers
2026-10-02 18:26 ` [PATCH v1 11/13] perf test: Add a test for perf timechart --tui Ian Rogers
2026-10-02 18:26 ` [PATCH v1 12/13] perf timechart: Add a --live mode to the TUI Ian Rogers
2026-10-02 18:26 ` Ian Rogers [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261002182624.3259797-14-irogers@google.com \
--to=irogers@google.com \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alice.mei.rogers@gmail.com \
--cc=atrajeev@linux.ibm.com \
--cc=changbin.du@huawei.com \
--cc=dapeng1.mi@linux.intel.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=peterz@infradead.org \
--cc=tanze@kylinos.cn \
--cc=thomas.falcon@intel.com \
--cc=wutengda@huaweicloud.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®