summaryrefslogtreecommitdiff
path: root/tools/perf/python/SchedGui.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/perf/python/SchedGui.py')
-rwxr-xr-xtools/perf/python/SchedGui.py246
1 files changed, 246 insertions, 0 deletions
diff --git a/tools/perf/python/SchedGui.py b/tools/perf/python/SchedGui.py
new file mode 100755
index 000000000000..fa0d010fe528
--- /dev/null
+++ b/tools/perf/python/SchedGui.py
@@ -0,0 +1,246 @@
+#!/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)