diff --git a/src/myfasthtml/controls/Profiler.py b/src/myfasthtml/controls/Profiler.py
index 71cb7ae..9f7565d 100644
--- a/src/myfasthtml/controls/Profiler.py
+++ b/src/myfasthtml/controls/Profiler.py
@@ -37,7 +37,7 @@ def _mk_span_rows(span, depth: int, total_ms: float) -> list:
"""
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)
@@ -57,7 +57,7 @@ def _mk_span_rows(span, depth: int, total_ms: float) -> list:
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)
@@ -75,7 +75,7 @@ def _mk_span_rows(span, depth: int, total_ms: float) -> list:
rows.append(row)
for child in span.children:
rows.extend(_mk_span_rows(child, depth + 1, total_ms))
-
+
return rows
@@ -111,7 +111,7 @@ class Commands(BaseCommands):
self._owner,
self._owner.handle_toggle_detail_view,
).htmx(target=f"#{self._id}")
-
+
def toggle_enable(self):
return Command(
"ProfilerToggleEnable",
@@ -145,7 +145,7 @@ class Commands(BaseCommands):
self._owner,
self._owner.handle_select_trace,
kwargs={"trace_id": trace_id},
- ).htmx(target=f"#{self._id}")
+ ).htmx(target=f"#tr_{trace_id}")
class Profiler(SingleInstance):
@@ -186,15 +186,24 @@ class Profiler(SingleInstance):
def handle_select_trace(self, trace_id: str):
"""Select a trace row and re-render to show it highlighted."""
+ if self._selected_id is not None:
+ old_trace = next(trace for trace in profiler.traces if trace.trace_id == self._selected_id)
+ else:
+ old_trace = None
+
self._selected_id = trace_id
- return self
+ trace = next(trace for trace in profiler.traces if trace.trace_id == trace_id)
+
+ return (self._mk_trace_item(trace),
+ self._mk_trace_item(old_trace),
+ self._panel.set_right(self._mk_right_panel(trace)))
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
@@ -245,32 +254,36 @@ class Profiler(SingleInstance):
id=f"tb_{self._id}",
)
+ def _mk_trace_item(self, trace: ProfilingTrace):
+ if trace is None:
+ return None
+
+ 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"
+
+ return 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,
+ id=f"tr_{trace.trace_id}",
+ ),
+ command=self.commands.select_trace(trace.trace_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)
+ rows = [self._mk_trace_item(trace) for trace in reversed(traces)]
return Div(
Div(
@@ -286,7 +299,7 @@ class Profiler(SingleInstance):
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.
@@ -305,12 +318,14 @@ class Profiler(SingleInstance):
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),
+ 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.
@@ -321,28 +336,28 @@ class Profiler(SingleInstance):
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",
+ "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(
@@ -354,13 +369,13 @@ class Profiler(SingleInstance):
),
_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.
@@ -376,6 +391,14 @@ class Profiler(SingleInstance):
cls="mf-profiler-detail",
)
+ def _mk_right_panel(self, trace: "ProfilingTrace"):
+ """Build the right panel with a trace detail view."""
+ return (
+ self._mk_detail_panel(trace)
+ if trace is not None
+ else self._mk_detail_placeholder()
+ )
+
# ------------------------------------------------------------------
# Render
# ------------------------------------------------------------------
@@ -386,15 +409,9 @@ class Profiler(SingleInstance):
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)
+ self._panel.set_right(self._mk_right_panel(selected_trace))
return Div(
self._mk_toolbar(),
self._panel,