Added Profiler control with basic UI
This commit is contained in:
@@ -272,7 +272,7 @@ class DataGrid(MultipleInstance):
|
||||
self._datagrid_filter.bind_command("ChangeFilterType", self.commands.filter())
|
||||
self._state.filtered[FILTER_INPUT_CID] = self._datagrid_filter.get_query()
|
||||
|
||||
# add Selection Selector
|
||||
# add Selection Selector (cell, row, column)
|
||||
selection_types = {
|
||||
"cell": mk.icon(grid, tooltip="Cell selection"), # default
|
||||
"row": mk.icon(row, tooltip="Row selection"),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from fastcore.basics import NotStr
|
||||
|
||||
from myfasthtml.core.constants import ColumnType
|
||||
from myfasthtml.core.constants import ColumnType, MediaActions
|
||||
from myfasthtml.core.utils import pascal_to_snake
|
||||
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
|
||||
number_row20_regular
|
||||
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
|
||||
checkbox_checked20_filled, math_formula16_regular, folder20_regular, document20_regular
|
||||
from myfasthtml.icons.fluent_p2 import text_field20_regular, text_bullet_list_square20_regular
|
||||
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
|
||||
checkbox_checked20_filled, math_formula16_regular, folder20_regular, document20_regular, pause_circle20_regular
|
||||
from myfasthtml.icons.fluent_p2 import text_field20_regular, text_bullet_list_square20_regular, play_circle20_regular, \
|
||||
dismiss_circle20_regular
|
||||
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular, record_stop20_regular
|
||||
|
||||
default_icons = {
|
||||
# default type icons
|
||||
@@ -22,6 +23,12 @@ default_icons = {
|
||||
"TreeViewFolder": folder20_regular,
|
||||
"TreeViewFile": document20_regular,
|
||||
|
||||
# Media
|
||||
MediaActions.Play: play_circle20_regular,
|
||||
MediaActions.Pause: pause_circle20_regular,
|
||||
MediaActions.Stop: record_stop20_regular,
|
||||
MediaActions.Cancel: dismiss_circle20_regular,
|
||||
|
||||
# Datagrid column icons
|
||||
ColumnType.RowIndex: number_symbol20_regular,
|
||||
ColumnType.Text: text_field20_regular,
|
||||
|
||||
@@ -104,7 +104,7 @@ class Panel(MultipleInstance):
|
||||
the panel with appropriate HTML elements and JavaScript for interactivity.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, conf: Optional[PanelConf] = None, _id=None):
|
||||
def __init__(self, parent, conf: Optional[PanelConf] = None, _id="-panel"):
|
||||
super().__init__(parent, _id=_id)
|
||||
self.conf = conf or PanelConf()
|
||||
self.commands = Commands(self)
|
||||
|
||||
202
src/myfasthtml/controls/Profiler.py
Normal file
202
src/myfasthtml/controls/Profiler.py
Normal 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()
|
||||
Reference in New Issue
Block a user