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.Properties import Properties, PropertiesConf 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 CumulativeSpan, ProfilingSpan, ProfilingTrace, profiler from myfasthtml.icons.fluent import ( arrow_clockwise20_regular, data_pie24_regular, text_bullet_list_tree20_filled, ) logger = logging.getLogger("Profiler") # --------------------------------------------------------------------------- # Span tree renderer — module-level, passed via PropertiesConf.types # --------------------------------------------------------------------------- def _mk_span_rows(span, depth: int, total_ms: float) -> list: """Recursively build span rows for the tree view. Args: span: A ProfilingSpan or CumulativeSpan to render. depth: Current nesting depth (controls indentation). total_ms: Reference duration used to compute bar widths. Returns: List of FT elements, one per span row (depth-first order). """ rows = [] indent = [Div(cls="mf-profiler-span-indent") for _ in range(depth)] if isinstance(span, CumulativeSpan): pct = (span.total_ms / total_ms * 100) if total_ms > 0 else 0 duration_cls = _span_duration_cls(span.total_ms) badge = Span( f"×{span.count} · min {span.min_ms:.2f} · avg {span.avg_ms:.2f} · max {span.max_ms:.2f} ms", cls="mf-profiler-cumulative-badge", ) row = Div( *indent, Div( Span(span.name, cls="mf-profiler-span-name"), Div(Div(style=f"width:{pct:.1f}%"), cls="mf-profiler-span-bar-bg"), Span(f"{span.total_ms:.1f} ms", cls=f"mf-profiler-span-ms {duration_cls}"), badge, cls="mf-profiler-span-body", ), cls="mf-profiler-span-row", ) rows.append(row) else: pct = (span.duration_ms / total_ms * 100) if total_ms > 0 else 0 duration_cls = _span_duration_cls(span.duration_ms) name_cls = "mf-profiler-span-name mf-profiler-span-name-root" if depth == 0 else "mf-profiler-span-name" row = Div( *indent, Div( Span(span.name, cls=name_cls), Div(Div(cls=f"mf-profiler-span-bar {duration_cls}", style=f"width:{pct:.1f}%"), cls="mf-profiler-span-bar-bg"), Span(f"{span.duration_ms:.1f} ms", cls=f"mf-profiler-span-ms {duration_cls}"), cls="mf-profiler-span-body", ), cls="mf-profiler-span-row", ) rows.append(row) for child in span.children: rows.extend(_mk_span_rows(child, depth + 1, total_ms)) return rows def _span_duration_cls(duration_ms: float) -> str: """Return the CSS modifier class for a span duration.""" if duration_ms < 20: return "mf-profiler-fast" if duration_ms < 100: return "mf-profiler-medium" return "mf-profiler-slow" def _span_tree_renderer(span: ProfilingSpan, trace: ProfilingTrace): """Renderer for ProfilingSpan values in a PropertiesConf.types mapping. Args: span: The root span to render as a tree. trace: The parent trace, used to compute proportional bar widths. Returns: A FT element containing the full span tree. """ rows = _mk_span_rows(span, 0, trace.total_duration_ms) return Div(*rows, cls="mf-profiler-span-tree-content") class Commands(BaseCommands): def toggle_detail_view(self): return Command( "ProfilerToggleDetailView", "Switch between tree and pie view", self._owner, self._owner.handle_toggle_detail_view, ).htmx(target=f"#{self._id}") 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._panel.set_side_visible("right", True) self._selected_id: str | None = None self._detail_view: str = "tree" 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_toggle_detail_view(self): """Toggle detail panel between tree and pie view.""" self._detail_view = "pie" if self._detail_view == "tree" else "tree" logger.debug(f"Profiler detail view set to {self._detail_view}") return self def handle_refresh(self): """Refresh the trace list without changing selection.""" 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 reversed(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") def _mk_detail_header(self, trace: "ProfilingTrace"): """Build the detail panel header with title and tree/pie toggle. Args: trace: The selected trace. Returns: A FT element for the detail header. """ duration_cls = self._duration_cls(trace.total_duration_ms) title = Div( Span(trace.command_name, cls="mf-profiler-detail-cmd"), Span(f" — {trace.total_duration_ms:.1f} ms", cls=f"mf-profiler-detail-duration {duration_cls}"), cls="mf-profiler-detail-title", ) tree_cls = "mf-profiler-view-btn mf-profiler-view-btn-active" if self._detail_view == "tree" else "mf-profiler-view-btn" pie_cls = "mf-profiler-view-btn mf-profiler-view-btn-active" if self._detail_view == "pie" else "mf-profiler-view-btn" toggle = Div( mk.icon(text_bullet_list_tree20_filled, command=self.commands.toggle_detail_view(), tooltip="Span tree", cls=tree_cls), mk.icon(data_pie24_regular, command=self.commands.toggle_detail_view(), tooltip="Pie chart (coming soon)", cls=pie_cls), cls="mf-profiler-view-toggle", ) return Div(title, toggle, cls="mf-profiler-detail-header") def _mk_detail_body(self, trace: "ProfilingTrace"): """Build the scrollable detail body: metadata, kwargs and span breakdown. Args: trace: The selected trace. Returns: A FT element for the detail body. """ from types import SimpleNamespace meta_props = Properties( self, conf=PropertiesConf( obj=trace, groups={"Metadata": { "command": "command_name", "description": "command_description", "duration_ms": "total_duration_ms", "timestamp": "timestamp", }}, ), _id="-detail-meta", ) kwargs_obj = SimpleNamespace(**trace.kwargs) if trace.kwargs else SimpleNamespace() kwargs_props = Properties( self, conf=PropertiesConf(obj=kwargs_obj, groups={"kwargs": {"*": ""}}), _id="-detail-kwargs", ) span_props = None if trace.root_span is not None: span_props = Properties( self, conf=PropertiesConf( obj=trace, groups={"Span breakdown": {"root_span": "root_span"}}, types={ProfilingSpan: _span_tree_renderer}, ), _id="-detail-spans", ) if self._detail_view == "pie": pie_placeholder = Div("Pie chart — coming soon.", cls="mf-profiler-empty") return Div(meta_props, kwargs_props, pie_placeholder, cls="mf-profiler-detail-body") return Div(meta_props, kwargs_props, span_props, cls="mf-profiler-detail-body") def _mk_detail_panel(self, trace: "ProfilingTrace"): """Build the full detail panel for a selected trace. Args: trace: The selected trace. Returns: A FT element for the detail panel. """ return Div( self._mk_detail_header(trace), self._mk_detail_body(trace), cls="mf-profiler-detail", ) # ------------------------------------------------------------------ # Render # ------------------------------------------------------------------ def render(self): selected_trace = None if self._selected_id is not None: selected_trace = next( (t for t in profiler.traces if t.trace_id == self._selected_id), None ) right_panel = ( self._mk_detail_panel(selected_trace) if selected_trace is not None else self._mk_detail_placeholder() ) self._panel.set_main(self._mk_trace_list()) self._panel.set_right(right_panel) return Div( self._mk_toolbar(), self._panel, id=self._id, cls="mf-profiler", ) def __ft__(self): return self.render()