#!/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 # # 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)