Added Profiler control with basic UI

This commit is contained in:
2026-03-22 08:32:40 +01:00
parent 72d6cce6ff
commit b8fd4e5ed1
14 changed files with 3440 additions and 1105 deletions

View File

@@ -0,0 +1,202 @@
import logging
from fasthtml.components import Div, Span
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.IconsHelper import IconsHelper
from myfasthtml.controls.Panel import Panel, PanelConf
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.constants import PROFILER_MAX_TRACES, MediaActions
from myfasthtml.core.instances import SingleInstance
from myfasthtml.core.profiler import profiler
from myfasthtml.icons.fluent import arrow_clockwise20_regular
logger = logging.getLogger("Profiler")
class Commands(BaseCommands):
def toggle_enable(self):
return Command(
"ProfilerToggleEnable",
"Enable / Disable profiler",
self._owner,
self._owner.handle_toggle_enable,
).htmx(target=f"#{self._id}")
def clear_traces(self):
return Command(
"ProfilerClearTraces",
"Clear all recorded traces",
self._owner,
self._owner.handle_clear_traces,
icon=IconsHelper.get(MediaActions.Cancel),
).htmx(target=f"#{self._id}")
def refresh(self):
return Command(
"ProfilerRefresh",
"Refresh traces",
self._owner,
self._owner.handle_refresh,
icon=arrow_clockwise20_regular,
).htmx(target=f"#{self._id}")
def select_trace(self, trace_id: str):
return Command(
"ProfilerSelectTrace",
"Display trace details",
self._owner,
self._owner.handle_select_trace,
kwargs={"trace_id": trace_id},
).htmx(target=f"#{self._id}")
class Profiler(SingleInstance):
"""In-application profiler UI.
Displays all recorded traces in a scrollable list (left) and trace
details in a resizable panel (right). The toolbar provides enable /
disable toggle and clear actions via icon-only buttons.
Attributes:
commands: Commands exposed by this control.
"""
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id)
self._panel = Panel(self, conf=PanelConf(show_right_title=False, show_display_right=False))
self._selected_id: str | None = None
self.commands = Commands(self)
logger.debug(f"Profiler created with id={self._id}")
# ------------------------------------------------------------------
# Command handlers
# ------------------------------------------------------------------
def handle_toggle_enable(self):
"""Toggle profiler.enabled and re-render."""
profiler.enabled = not profiler.enabled
logger.debug(f"Profiler enabled set to {profiler.enabled}")
return self
def handle_clear_traces(self):
"""Clear the trace buffer and re-render."""
profiler.clear()
logger.debug("Profiler traces cleared from UI")
return self
def handle_select_trace(self, trace_id: str):
"""Select a trace row and re-render to show it highlighted."""
self._selected_id = trace_id
return self
def handle_refresh(self):
"""Select a trace row and re-render to show it highlighted."""
return self
# ------------------------------------------------------------------
# Private rendering helpers
# ------------------------------------------------------------------
def _duration_cls(self, duration_ms: float) -> str:
"""Return the CSS modifier class for a given duration."""
if duration_ms < 20:
return "mf-profiler-fast"
if duration_ms < 100:
return "mf-profiler-medium"
return "mf-profiler-slow"
def _mk_toolbar(self):
"""Build the icon toolbar with enable/disable, clear and overhead metrics."""
enable_icon = (
IconsHelper.get(MediaActions.Pause)
if profiler.enabled
else IconsHelper.get(MediaActions.Play)
)
enable_tooltip = "Disable profiler" if profiler.enabled else "Enable profiler"
overhead = (
f"Overhead/span: {profiler.overhead_per_span_us:.1f} µs "
f"Total: {profiler.total_overhead_ms:.3f} ms "
f"Traces: {len(profiler.traces)} / {PROFILER_MAX_TRACES}"
)
return Div(
mk.icon(
enable_icon,
command=self.commands.toggle_enable(),
tooltip=enable_tooltip,
),
mk.icon(
command=self.commands.clear_traces(),
tooltip="Clear traces",
cls="mf-profiler-btn-danger",
),
mk.icon(
command=self.commands.refresh(),
),
Span(overhead, cls="mf-profiler-overhead"),
cls="mf-profiler-toolbar",
id=f"tb_{self._id}",
)
def _mk_trace_list(self):
"""Build the trace list with one clickable row per recorded trace."""
traces = profiler.traces
if not traces:
return Div("No traces recorded.", cls="mf-profiler-empty")
rows = []
for trace in traces:
ts = trace.timestamp.strftime("%H:%M:%S.") + f"{trace.timestamp.microsecond // 1000:03d}"
duration_cls = self._duration_cls(trace.total_duration_ms)
row_cls = "mf-profiler-row mf-profiler-row-selected" if trace.trace_id == self._selected_id else "mf-profiler-row"
row = mk.mk(
Div(
Div(
Span(trace.command_name, cls="mf-profiler-cmd"),
Span(trace.command_description, cls="mf-profiler-cmd-description"),
cls="mf-profiler-cmd-cell",
),
Span(f"{trace.total_duration_ms:.1f} ms", cls=f"mf-profiler-duration {duration_cls}"),
Span(ts, cls="mf-profiler-ts"),
cls=row_cls,
),
command=self.commands.select_trace(trace.trace_id),
)
rows.append(row)
return Div(
Div(
Span("Command", cls="mf-profiler-col-header"),
Span("Duration", cls="mf-profiler-col-header mf-profiler-col-right"),
Span("Time", cls="mf-profiler-col-header mf-profiler-col-right"),
cls="mf-profiler-list-header",
),
Div(*rows, cls="mf-profiler-list-body"),
cls="mf-profiler-list",
)
def _mk_detail_placeholder(self):
"""Placeholder shown in the right panel before a trace is selected."""
return Div("Select a trace to view details.", cls="mf-profiler-empty")
# ------------------------------------------------------------------
# Render
# ------------------------------------------------------------------
def render(self):
self._panel.set_main(self._mk_trace_list())
self._panel.set_right(self._mk_detail_placeholder())
return Div(
self._mk_toolbar(),
self._panel,
id=self._id,
cls="mf-profiler",
)
def __ft__(self):
return self.render()