mirror of
https://github.com/Hopiu/bowser.git
synced 2026-03-16 19:10:24 +00:00
Add profiling and layout caching to improve rendering performance
This commit is contained in:
parent
c2533a33c0
commit
e5e281dfc2
1 changed files with 177 additions and 65 deletions
|
|
@ -3,12 +3,12 @@
|
||||||
import gi
|
import gi
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import logging
|
import logging
|
||||||
from functools import partial
|
import cairo
|
||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("Adw", "1")
|
gi.require_version("Adw", "1")
|
||||||
|
|
||||||
from gi.repository import Gtk, Gdk, GdkPixbuf, Adw
|
from gi.repository import Gtk, Gdk, Adw
|
||||||
import skia
|
import skia
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,6 +36,10 @@ class Chrome:
|
||||||
self.frame_times = [] # List of recent frame timestamps
|
self.frame_times = [] # List of recent frame timestamps
|
||||||
self.fps = 0.0
|
self.fps = 0.0
|
||||||
|
|
||||||
|
# Profiling data
|
||||||
|
self._last_profile = {}
|
||||||
|
self._last_profile_total = 0.0
|
||||||
|
|
||||||
# Scroll state
|
# Scroll state
|
||||||
self.scroll_y = 0
|
self.scroll_y = 0
|
||||||
self.document_height = 0 # Total document height for scroll limits
|
self.document_height = 0 # Total document height for scroll limits
|
||||||
|
|
@ -54,6 +58,16 @@ class Chrome:
|
||||||
# Each entry: {text, x, y, width, height, font_size, font, char_positions}
|
# Each entry: {text, x, y, width, height, font_size, font, char_positions}
|
||||||
# char_positions is a list of x offsets for each character
|
# char_positions is a list of x offsets for each character
|
||||||
self.text_layout = []
|
self.text_layout = []
|
||||||
|
|
||||||
|
# Layout cache to avoid recalculation on scroll
|
||||||
|
self._layout_cache_width = 0
|
||||||
|
self._layout_cache_doc_id = None
|
||||||
|
self._layout_blocks = [] # Cached processed blocks
|
||||||
|
self._layout_rects = [] # Cached debug rects
|
||||||
|
|
||||||
|
# Font cache to avoid recreating fonts every frame
|
||||||
|
self._font_cache = {} # {font_size: skia.Font}
|
||||||
|
self._default_typeface = None
|
||||||
|
|
||||||
def create_window(self):
|
def create_window(self):
|
||||||
"""Initialize the Adwaita application window."""
|
"""Initialize the Adwaita application window."""
|
||||||
|
|
@ -317,50 +331,77 @@ class Chrome:
|
||||||
if len(self.frame_times) > 1:
|
if len(self.frame_times) > 1:
|
||||||
self.fps = len(self.frame_times)
|
self.fps = len(self.frame_times)
|
||||||
|
|
||||||
self.logger.debug(f"on_draw start {width}x{height}")
|
# Profiling timers
|
||||||
|
profile_start = time.perf_counter()
|
||||||
|
timings = {}
|
||||||
|
|
||||||
# Create Skia surface for this frame
|
# Create Skia surface for this frame
|
||||||
|
t0 = time.perf_counter()
|
||||||
self.skia_surface = skia.Surface(width, height)
|
self.skia_surface = skia.Surface(width, height)
|
||||||
canvas = self.skia_surface.getCanvas()
|
canvas = self.skia_surface.getCanvas()
|
||||||
|
timings['surface_create'] = time.perf_counter() - t0
|
||||||
|
|
||||||
# Store viewport height
|
# Store viewport height
|
||||||
self.viewport_height = height
|
self.viewport_height = height
|
||||||
|
|
||||||
# White background
|
# White background
|
||||||
|
t0 = time.perf_counter()
|
||||||
canvas.clear(skia.ColorWHITE)
|
canvas.clear(skia.ColorWHITE)
|
||||||
|
timings['clear'] = time.perf_counter() - t0
|
||||||
|
|
||||||
# Render DOM content
|
# Render DOM content
|
||||||
frame = self.browser.active_tab.main_frame if self.browser.active_tab else None
|
frame = self.browser.active_tab.main_frame if self.browser.active_tab else None
|
||||||
document = frame.document if frame else None
|
document = frame.document if frame else None
|
||||||
if document:
|
if document:
|
||||||
|
t0 = time.perf_counter()
|
||||||
self._render_dom_content(canvas, document, width, height)
|
self._render_dom_content(canvas, document, width, height)
|
||||||
# Draw scrollbar on top (in screen coordinates, not scrolled)
|
timings['render_dom'] = time.perf_counter() - t0
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
self._draw_scrollbar(canvas, width, height)
|
self._draw_scrollbar(canvas, width, height)
|
||||||
# Draw FPS counter in debug mode
|
timings['scrollbar'] = time.perf_counter() - t0
|
||||||
|
|
||||||
if self.debug_mode:
|
if self.debug_mode:
|
||||||
self._draw_fps_counter(canvas, width)
|
self._draw_fps_counter(canvas, width)
|
||||||
else:
|
else:
|
||||||
paint = skia.Paint()
|
paint = skia.Paint()
|
||||||
paint.setAntiAlias(True)
|
paint.setAntiAlias(True)
|
||||||
paint.setColor(skia.ColorBLACK)
|
paint.setColor(skia.ColorBLACK)
|
||||||
font = skia.Font(skia.Typeface.MakeDefault(), 20)
|
font = self._get_font(20)
|
||||||
canvas.drawString("Bowser — Enter a URL to browse", 20, 50, font, paint)
|
canvas.drawString("Bowser — Enter a URL to browse", 20, 50, font, paint)
|
||||||
|
|
||||||
# Convert Skia surface to GTK Pixbuf and blit to Cairo context
|
# Get raw pixel data from Skia surface
|
||||||
|
t0 = time.perf_counter()
|
||||||
image = self.skia_surface.makeImageSnapshot()
|
image = self.skia_surface.makeImageSnapshot()
|
||||||
png_data = image.encodeToData().bytes()
|
timings['snapshot'] = time.perf_counter() - t0
|
||||||
|
|
||||||
# Load PNG data into a Pixbuf
|
t0 = time.perf_counter()
|
||||||
from io import BytesIO
|
pixels = image.tobytes()
|
||||||
|
timings['tobytes'] = time.perf_counter() - t0
|
||||||
loader = GdkPixbuf.PixbufLoader.new_with_type("png")
|
|
||||||
loader.write(png_data)
|
# Create Cairo ImageSurface from raw pixels
|
||||||
loader.close()
|
t0 = time.perf_counter()
|
||||||
pixbuf = loader.get_pixbuf()
|
cairo_surface = cairo.ImageSurface.create_for_data(
|
||||||
|
bytearray(pixels),
|
||||||
# Render pixbuf to Cairo context
|
cairo.FORMAT_ARGB32,
|
||||||
Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
|
width,
|
||||||
|
height,
|
||||||
|
width * 4 # stride
|
||||||
|
)
|
||||||
|
timings['cairo_surface'] = time.perf_counter() - t0
|
||||||
|
|
||||||
|
# Blit Cairo surface to context
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
context.set_source_surface(cairo_surface, 0, 0)
|
||||||
context.paint()
|
context.paint()
|
||||||
self.logger.debug("on_draw end")
|
timings['cairo_blit'] = time.perf_counter() - t0
|
||||||
|
|
||||||
|
total_time = time.perf_counter() - profile_start
|
||||||
|
|
||||||
|
# Store profiling data for debug display
|
||||||
|
if self.debug_mode:
|
||||||
|
self._last_profile = timings
|
||||||
|
self._last_profile_total = total_time
|
||||||
|
|
||||||
def _render_dom_content(self, canvas, document, width: int, height: int):
|
def _render_dom_content(self, canvas, document, width: int, height: int):
|
||||||
"""Render a basic DOM tree with headings, paragraphs, and lists."""
|
"""Render a basic DOM tree with headings, paragraphs, and lists."""
|
||||||
|
|
@ -370,28 +411,74 @@ class Chrome:
|
||||||
if not body:
|
if not body:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clear text layout for this render
|
# Check if we need to rebuild layout cache
|
||||||
self.text_layout = []
|
doc_id = id(document)
|
||||||
|
needs_rebuild = (
|
||||||
|
self._layout_cache_doc_id != doc_id or
|
||||||
|
self._layout_cache_width != width or
|
||||||
|
not self.text_layout
|
||||||
|
)
|
||||||
|
|
||||||
|
if needs_rebuild:
|
||||||
|
self._rebuild_layout(body, width)
|
||||||
|
self._layout_cache_doc_id = doc_id
|
||||||
|
self._layout_cache_width = width
|
||||||
|
self.logger.debug(f"Layout rebuilt: {len(self.text_layout)} lines")
|
||||||
|
|
||||||
# Apply scroll offset
|
# Apply scroll offset
|
||||||
canvas.save()
|
canvas.save()
|
||||||
canvas.translate(0, -self.scroll_y)
|
canvas.translate(0, -self.scroll_y)
|
||||||
|
|
||||||
blocks = self._collect_blocks(body)
|
|
||||||
paint = skia.Paint()
|
paint = skia.Paint()
|
||||||
paint.setAntiAlias(True)
|
paint.setAntiAlias(True)
|
||||||
paint.setColor(skia.ColorBLACK)
|
paint.setColor(skia.ColorBLACK)
|
||||||
|
|
||||||
|
# Only draw visible lines
|
||||||
|
visible_top = self.scroll_y - 50
|
||||||
|
visible_bottom = self.scroll_y + height + 50
|
||||||
|
|
||||||
|
visible_count = 0
|
||||||
|
for line_info in self.text_layout:
|
||||||
|
line_y = line_info["y"] + line_info["font_size"] # Baseline y
|
||||||
|
if line_y < visible_top or line_y - line_info["height"] > visible_bottom:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visible_count += 1
|
||||||
|
font = self._get_font(line_info["font_size"])
|
||||||
|
canvas.drawString(line_info["text"], line_info["x"], line_y, font, paint)
|
||||||
|
|
||||||
|
# Draw selection highlight
|
||||||
|
if self.selection_start and self.selection_end:
|
||||||
|
self._draw_text_selection(canvas)
|
||||||
|
|
||||||
|
# Draw debug overlays
|
||||||
|
if self.debug_mode:
|
||||||
|
self._draw_debug_overlays(canvas, self._layout_rects, document)
|
||||||
|
|
||||||
|
canvas.restore()
|
||||||
|
|
||||||
|
def _get_font(self, size: int):
|
||||||
|
"""Get a cached font for the given size."""
|
||||||
|
if size not in self._font_cache:
|
||||||
|
if self._default_typeface is None:
|
||||||
|
self._default_typeface = skia.Typeface.MakeDefault()
|
||||||
|
self._font_cache[size] = skia.Font(self._default_typeface, size)
|
||||||
|
return self._font_cache[size]
|
||||||
|
|
||||||
|
def _rebuild_layout(self, body, width: int):
|
||||||
|
"""Rebuild the layout cache for text positioning."""
|
||||||
|
self.text_layout = []
|
||||||
|
self._layout_rects = []
|
||||||
|
|
||||||
|
blocks = self._collect_blocks(body)
|
||||||
|
|
||||||
x_margin = 20
|
x_margin = 20
|
||||||
max_width = max(10, width - 2 * x_margin)
|
max_width = max(10, width - 2 * x_margin)
|
||||||
y = 30
|
y = 30
|
||||||
|
|
||||||
# Track layout for debug mode
|
|
||||||
layout_rects = []
|
|
||||||
|
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
font_size = block.get("font_size", 14)
|
font_size = block.get("font_size", 14)
|
||||||
font = skia.Font(skia.Typeface.MakeDefault(), font_size)
|
font = self._get_font(font_size)
|
||||||
text = block.get("text", "")
|
text = block.get("text", "")
|
||||||
if not text:
|
if not text:
|
||||||
y += font_size * 0.6
|
y += font_size * 0.6
|
||||||
|
|
@ -424,11 +511,6 @@ class Chrome:
|
||||||
|
|
||||||
block_start_y = y
|
block_start_y = y
|
||||||
for line in lines:
|
for line in lines:
|
||||||
# Only render if visible (accounting for scroll)
|
|
||||||
visible_y = y - self.scroll_y
|
|
||||||
if visible_y > -50 and visible_y < height + 50:
|
|
||||||
canvas.drawString(line, x_margin, y, font, paint)
|
|
||||||
|
|
||||||
# Calculate character positions for precise selection
|
# Calculate character positions for precise selection
|
||||||
char_positions = [0.0] # Start at 0
|
char_positions = [0.0] # Start at 0
|
||||||
for i in range(1, len(line) + 1):
|
for i in range(1, len(line) + 1):
|
||||||
|
|
@ -452,28 +534,17 @@ class Chrome:
|
||||||
y += block.get("margin_bottom", 10)
|
y += block.get("margin_bottom", 10)
|
||||||
|
|
||||||
# Store layout for debug mode
|
# Store layout for debug mode
|
||||||
if self.debug_mode:
|
block_type = block.get("block_type", "block")
|
||||||
block_type = block.get("block_type", "block")
|
self._layout_rects.append({
|
||||||
layout_rects.append({
|
"x": x_margin - 5,
|
||||||
"x": x_margin - 5,
|
"y": block_start_y - font_size,
|
||||||
"y": block_start_y - font_size,
|
"width": max_width + 10,
|
||||||
"width": max_width + 10,
|
"height": block_end_y - block_start_y + 5,
|
||||||
"height": block_end_y - block_start_y + 5,
|
"type": block_type
|
||||||
"type": block_type
|
})
|
||||||
})
|
|
||||||
|
|
||||||
# Store total document height
|
# Store total document height
|
||||||
self.document_height = y + 50 # Add some padding at the bottom
|
self.document_height = y + 50 # Add some padding at the bottom
|
||||||
|
|
||||||
# Draw selection highlight based on text layout
|
|
||||||
if self.selection_start and self.selection_end:
|
|
||||||
self._draw_text_selection(canvas)
|
|
||||||
|
|
||||||
# Draw debug overlays
|
|
||||||
if self.debug_mode:
|
|
||||||
self._draw_debug_overlays(canvas, layout_rects, document)
|
|
||||||
|
|
||||||
canvas.restore()
|
|
||||||
|
|
||||||
def _find_body(self, document):
|
def _find_body(self, document):
|
||||||
from ..parser.html import Element
|
from ..parser.html import Element
|
||||||
|
|
@ -610,7 +681,7 @@ class Chrome:
|
||||||
legend_x = 10
|
legend_x = 10
|
||||||
legend_y = self.scroll_y + 10
|
legend_y = self.scroll_y + 10
|
||||||
|
|
||||||
font = skia.Font(skia.Typeface.MakeDefault(), 11)
|
font = self._get_font(11)
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
bg_paint = skia.Paint()
|
bg_paint = skia.Paint()
|
||||||
|
|
@ -639,32 +710,73 @@ class Chrome:
|
||||||
y_offset += 18
|
y_offset += 18
|
||||||
|
|
||||||
def _draw_fps_counter(self, canvas, width: int):
|
def _draw_fps_counter(self, canvas, width: int):
|
||||||
"""Draw FPS counter in top-right corner."""
|
"""Draw FPS counter and profiling info in top-right corner."""
|
||||||
# Position in top-right
|
font = self._get_font(11)
|
||||||
fps_x = width - 80
|
small_font = self._get_font(9)
|
||||||
fps_y = 10
|
|
||||||
|
|
||||||
font = skia.Font(skia.Typeface.MakeDefault(), 14)
|
# Calculate panel size based on profile data
|
||||||
|
panel_width = 180
|
||||||
|
num_profile_lines = len(self._last_profile) + 2 # +2 for FPS and total
|
||||||
|
panel_height = 18 + num_profile_lines * 12
|
||||||
|
|
||||||
|
# Position in top-right
|
||||||
|
panel_x = width - panel_width - 10
|
||||||
|
panel_y = 10
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
bg_paint = skia.Paint()
|
bg_paint = skia.Paint()
|
||||||
bg_paint.setColor(skia.Color(0, 0, 0, 180))
|
bg_paint.setColor(skia.Color(0, 0, 0, 200))
|
||||||
bg_paint.setStyle(skia.Paint.kFill_Style)
|
bg_paint.setStyle(skia.Paint.kFill_Style)
|
||||||
canvas.drawRect(skia.Rect.MakeLTRB(fps_x - 5, fps_y, fps_x + 75, fps_y + 25), bg_paint)
|
canvas.drawRect(skia.Rect.MakeLTRB(
|
||||||
|
panel_x, panel_y,
|
||||||
|
panel_x + panel_width, panel_y + panel_height
|
||||||
|
), bg_paint)
|
||||||
|
|
||||||
# FPS text with color based on performance
|
|
||||||
text_paint = skia.Paint()
|
text_paint = skia.Paint()
|
||||||
text_paint.setAntiAlias(True)
|
text_paint.setAntiAlias(True)
|
||||||
|
|
||||||
|
# FPS with color
|
||||||
if self.fps >= 50:
|
if self.fps >= 50:
|
||||||
text_paint.setColor(skia.Color(100, 255, 100, 255)) # Green
|
text_paint.setColor(skia.Color(100, 255, 100, 255))
|
||||||
elif self.fps >= 30:
|
elif self.fps >= 30:
|
||||||
text_paint.setColor(skia.Color(255, 255, 100, 255)) # Yellow
|
text_paint.setColor(skia.Color(255, 255, 100, 255))
|
||||||
else:
|
else:
|
||||||
text_paint.setColor(skia.Color(255, 100, 100, 255)) # Red
|
text_paint.setColor(skia.Color(255, 100, 100, 255))
|
||||||
|
|
||||||
fps_text = f"FPS: {self.fps:.0f}"
|
y = panel_y + 14
|
||||||
canvas.drawString(fps_text, fps_x, fps_y + 17, font, text_paint)
|
canvas.drawString(f"FPS: {self.fps:.0f}", panel_x + 5, y, font, text_paint)
|
||||||
|
|
||||||
|
# Total frame time
|
||||||
|
text_paint.setColor(skia.ColorWHITE)
|
||||||
|
total_ms = self._last_profile_total * 1000
|
||||||
|
y += 14
|
||||||
|
canvas.drawString(f"Frame: {total_ms:.1f}ms", panel_x + 5, y, font, text_paint)
|
||||||
|
|
||||||
|
# Profile breakdown
|
||||||
|
if self._last_profile:
|
||||||
|
gray_paint = skia.Paint()
|
||||||
|
gray_paint.setAntiAlias(True)
|
||||||
|
gray_paint.setColor(skia.Color(180, 180, 180, 255))
|
||||||
|
|
||||||
|
# Sort by time descending
|
||||||
|
sorted_items = sorted(
|
||||||
|
self._last_profile.items(),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for name, duration in sorted_items:
|
||||||
|
y += 12
|
||||||
|
ms = duration * 1000
|
||||||
|
pct = (duration / self._last_profile_total * 100) if self._last_profile_total > 0 else 0
|
||||||
|
# Color code: red if >50% of frame time
|
||||||
|
if pct > 50:
|
||||||
|
gray_paint.setColor(skia.Color(255, 150, 150, 255))
|
||||||
|
elif pct > 25:
|
||||||
|
gray_paint.setColor(skia.Color(255, 220, 150, 255))
|
||||||
|
else:
|
||||||
|
gray_paint.setColor(skia.Color(180, 180, 180, 255))
|
||||||
|
canvas.drawString(f"{name}: {ms:.1f}ms ({pct:.0f}%)", panel_x + 8, y, small_font, gray_paint)
|
||||||
|
|
||||||
def paint(self):
|
def paint(self):
|
||||||
"""Trigger redraw of the drawing area."""
|
"""Trigger redraw of the drawing area."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue