mirror of
https://github.com/torvalds/linux.git
synced 2026-11-10 17:11:02 +01:00
Port sched-migration.py and SchedGui.py from tools/perf/scripts/python/ to standalone modules in tools/perf/python/: - Refactor sched-migration.py into a SchedMigrationAnalyzer class using perf.session for event processing and add argparse CLI support (-i/--input, -v/--verbose, --gui, --no-gui). - Port SchedGui.py to tools/perf/python/ as a local module dependency. - Load wxPython dynamically via importlib only when GUI mode (--gui) is requested so text-mode analysis, testing on headless systems, and static analysis (mypy/pylint) succeed without wx installed. - Remove Python 2 compatibility code. Add a shell test (test_sched_migration_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
247 lines
8.5 KiB
Python
Executable File
247 lines
8.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
# SchedGui.py - Python extension for perf script, basic GUI code for
|
|
# traces drawing and overview.
|
|
#
|
|
# Copyright (C) 2010 by Frederic Weisbecker <fweisbec@gmail.com>
|
|
#
|
|
# Ported to modern directory structure.
|
|
|
|
"""SchedGui.py - Python extension for perf script, basic GUI code for traces drawing and overview."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from typing import Any
|
|
|
|
class _DummyWx:
|
|
"""Dummy wx module fallback when wxPython is not installed."""
|
|
Frame = object
|
|
|
|
|
|
try:
|
|
wx: Any = importlib.import_module("wx")
|
|
WX_AVAILABLE = True
|
|
except ImportError:
|
|
wx = _DummyWx
|
|
WX_AVAILABLE = False
|
|
|
|
|
|
class RootFrame(wx.Frame):
|
|
"""Main window frame for scheduling trace visualization."""
|
|
Y_OFFSET = 100
|
|
RECT_HEIGHT = 100
|
|
RECT_SPACE = 50
|
|
EVENT_MARKING_WIDTH = 5
|
|
|
|
def __init__(self, sched_tracer, title, parent=None, win_id=-1):
|
|
if not WX_AVAILABLE:
|
|
raise ImportError("You need to install the wxpython lib for this script")
|
|
wx.Frame.__init__(self, parent, win_id, title)
|
|
|
|
self.dc = None
|
|
(self.screen_width, self.screen_height) = wx.GetDisplaySize()
|
|
self.screen_width -= 10
|
|
self.screen_height -= 10
|
|
self.zoom = 0.5
|
|
self.scroll_scale = 20
|
|
self.sched_tracer = sched_tracer
|
|
self.sched_tracer.set_root_win(self)
|
|
(self.ts_start, self.ts_end) = sched_tracer.interval()
|
|
self.update_width_virtual()
|
|
self.nr_rects = sched_tracer.nr_rectangles() + 1
|
|
self.height_virtual = RootFrame.Y_OFFSET + \
|
|
(self.nr_rects * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE))
|
|
|
|
# whole window panel
|
|
self.panel = wx.Panel(self, size=(self.screen_width, self.screen_height))
|
|
|
|
# scrollable container
|
|
# Create SplitterWindow
|
|
self.splitter = wx.SplitterWindow(self.panel, style=wx.SP_3D)
|
|
|
|
# scrollable container (Top)
|
|
self.scroll = wx.ScrolledWindow(self.splitter)
|
|
self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale,
|
|
int(self.width_virtual // self.scroll_scale),
|
|
int(self.height_virtual // self.scroll_scale))
|
|
self.scroll.EnableScrolling(True, True)
|
|
self.scroll.SetFocus()
|
|
|
|
# scrollable drawing area
|
|
self.scroll_panel = wx.Panel(self.scroll,
|
|
size=(self.screen_width - 15, self.screen_height // 2))
|
|
self.scroll_panel.Bind(wx.EVT_PAINT, self.on_paint)
|
|
self.scroll_panel.Bind(wx.EVT_KEY_DOWN, self.on_key_press)
|
|
self.scroll_panel.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
|
|
self.scroll.Bind(wx.EVT_KEY_DOWN, self.on_key_press)
|
|
self.scroll.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
|
|
|
|
self.scroll_panel.SetSize(int(self.width_virtual), int(self.height_virtual))
|
|
|
|
# Create a separate panel for text (Bottom)
|
|
self.text_panel = wx.Panel(self.splitter)
|
|
self.text_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.txt = wx.TextCtrl(self.text_panel, -1, "Click a bar to see details",
|
|
style=wx.TE_MULTILINE)
|
|
self.text_sizer.Add(self.txt, 1, wx.EXPAND | wx.ALL, 5)
|
|
self.text_panel.SetSizer(self.text_sizer)
|
|
|
|
# Split the window
|
|
self.splitter.SplitHorizontally(self.scroll, self.text_panel, (self.screen_height * 3) // 4)
|
|
|
|
# Main sizer to layout splitter
|
|
self.main_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
self.main_sizer.Add(self.splitter, 1, wx.EXPAND)
|
|
self.panel.SetSizer(self.main_sizer)
|
|
|
|
self.scroll.Fit()
|
|
self.Fit()
|
|
|
|
self.Show(True)
|
|
|
|
def us_to_px(self, val):
|
|
"""Convert microseconds to pixels."""
|
|
return val / (10 ** 3) * self.zoom
|
|
|
|
def px_to_us(self, val):
|
|
"""Convert pixels to microseconds."""
|
|
return (val / self.zoom) * (10 ** 3)
|
|
|
|
def scroll_start(self):
|
|
"""Get scroll start position in pixels."""
|
|
(x, y) = self.scroll.GetViewStart()
|
|
return (x * self.scroll_scale, y * self.scroll_scale)
|
|
|
|
def scroll_start_us(self):
|
|
"""Get scroll start position in microseconds."""
|
|
(x, _) = self.scroll_start()
|
|
return self.px_to_us(x)
|
|
|
|
def paint_rectangle_zone(self, nr, color, top_color, start, end):
|
|
"""Draw a rectangle zone for a CPU."""
|
|
offset_px = self.us_to_px(start - self.ts_start)
|
|
width_px = self.us_to_px(end - start)
|
|
|
|
offset_py = RootFrame.Y_OFFSET + (nr * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE))
|
|
width_py = RootFrame.RECT_HEIGHT
|
|
|
|
dc = self.dc
|
|
|
|
if top_color is not None:
|
|
(r, g, b) = top_color
|
|
top_color = wx.Colour(r, g, b)
|
|
brush = wx.Brush(top_color, wx.SOLID)
|
|
dc.SetBrush(brush)
|
|
dc.DrawRectangle(int(offset_px), int(offset_py),
|
|
int(width_px), RootFrame.EVENT_MARKING_WIDTH)
|
|
width_py -= RootFrame.EVENT_MARKING_WIDTH
|
|
offset_py += RootFrame.EVENT_MARKING_WIDTH
|
|
|
|
(r, g, b) = color
|
|
color = wx.Colour(r, g, b)
|
|
brush = wx.Brush(color, wx.SOLID)
|
|
dc.SetBrush(brush)
|
|
dc.DrawRectangle(int(offset_px), int(offset_py), int(width_px), int(width_py))
|
|
|
|
def update_rectangles(self, start, end):
|
|
"""Update rectangles in the given time window."""
|
|
start += self.ts_start
|
|
end += self.ts_start
|
|
self.sched_tracer.fill_zone(start, end)
|
|
|
|
def on_paint(self, event):
|
|
"""Handle paint event."""
|
|
window = event.GetEventObject()
|
|
dc = wx.PaintDC(window)
|
|
|
|
# Clear background to avoid ghosting
|
|
dc.SetBackground(wx.Brush(window.GetBackgroundColour()))
|
|
dc.Clear()
|
|
|
|
self.dc = dc
|
|
try:
|
|
width = min(self.width_virtual, self.screen_width)
|
|
(x, _) = self.scroll_start()
|
|
start = self.px_to_us(x)
|
|
end = self.px_to_us(x + width)
|
|
self.update_rectangles(start, end)
|
|
|
|
# Draw CPU labels at the left edge of the visible area
|
|
(x_scroll, _) = self.scroll_start()
|
|
for nr in range(self.nr_rects):
|
|
offset_py = RootFrame.Y_OFFSET + (nr * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE))
|
|
dc.DrawText(f"CPU {nr}", x_scroll + 10, offset_py + 10)
|
|
finally:
|
|
self.dc = None
|
|
|
|
def rect_from_ypixel(self, y):
|
|
y -= RootFrame.Y_OFFSET
|
|
rect = y // (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)
|
|
height = y % (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)
|
|
|
|
if rect < 0 or rect > self.nr_rects - 1 or height > RootFrame.RECT_HEIGHT:
|
|
return -1
|
|
|
|
return rect
|
|
|
|
def update_summary(self, txt):
|
|
self.txt.SetValue(txt)
|
|
self.text_panel.Layout()
|
|
self.splitter.Layout()
|
|
self.text_panel.Refresh()
|
|
|
|
def on_mouse_down(self, event):
|
|
pos = event.GetPosition()
|
|
x, y = pos.x, pos.y
|
|
rect = self.rect_from_ypixel(y)
|
|
if rect == -1:
|
|
return
|
|
|
|
t = self.px_to_us(x) + self.ts_start
|
|
|
|
self.sched_tracer.mouse_down(rect, t)
|
|
|
|
def update_width_virtual(self):
|
|
self.width_virtual = self.us_to_px(self.ts_end - self.ts_start)
|
|
|
|
def __zoom(self, x):
|
|
self.update_width_virtual()
|
|
(xpos, ypos) = self.scroll.GetViewStart()
|
|
xpos = int(self.us_to_px(x) // self.scroll_scale)
|
|
self.scroll_panel.SetSize((int(self.width_virtual), int(self.height_virtual)))
|
|
self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale,
|
|
int(self.width_virtual // self.scroll_scale),
|
|
int(self.height_virtual // self.scroll_scale),
|
|
xpos, ypos)
|
|
self.Refresh()
|
|
|
|
def zoom_in(self):
|
|
x = self.scroll_start_us()
|
|
self.zoom *= 2
|
|
self.__zoom(x)
|
|
|
|
def zoom_out(self):
|
|
x = self.scroll_start_us()
|
|
self.zoom /= 2
|
|
self.__zoom(x)
|
|
|
|
def on_key_press(self, event):
|
|
key = event.GetRawKeyCode()
|
|
if key == ord("+"):
|
|
self.zoom_in()
|
|
return
|
|
if key == ord("-"):
|
|
self.zoom_out()
|
|
return
|
|
|
|
key = event.GetKeyCode()
|
|
(x, y) = self.scroll.GetViewStart()
|
|
if key == wx.WXK_RIGHT:
|
|
self.scroll.Scroll(x + 1, y)
|
|
elif key == wx.WXK_LEFT:
|
|
self.scroll.Scroll(x - 1, y)
|
|
elif key == wx.WXK_DOWN:
|
|
self.scroll.Scroll(x, y + 1)
|
|
elif key == wx.WXK_UP:
|
|
self.scroll.Scroll(x, y - 1)
|