diff --git a/docs/Profiler.md b/docs/Profiler.md index 67934e5..3aae38a 100644 --- a/docs/Profiler.md +++ b/docs/Profiler.md @@ -7,6 +7,23 @@ per command call). The HTMX debug traces (via `htmx_debug.js`) confirmed the bot server-side. A persistent, in-application profiling system is needed for continuous analysis across sessions and future investigations. +--- + +## Implementation Status + +| Phase | Item | Status | +|-------|------|--------| +| **Phase 1 — Core** | `profiler.py` — data model + probe mechanisms | ✅ Done | +| **Phase 1 — Core** | `tests/core/test_profiler.py` — full test suite (7 classes) | ✅ Done | +| **Phase 1 — Core** | Hook `utils.py` — Level A `command_span` | ✅ Done | +| **Phase 1 — Core** | Hook `commands.py` — Level B phases | ⏳ Deferred | +| **Phase 2 — Controls** | `Profiler.py` — global layout (toolbar + list) | 🔄 In progress | +| **Phase 2 — Controls** | `Profiler.py` — detail panel (span tree + pie) | ⏳ Pending | +| **Phase 2 — Controls** | CSS `profiler.css` | 🔄 In progress | +| **Phase 2 — Controls** | `ProfilerPieChart.py` | ⏳ Future | + +--- + ## Design Decisions ### Data Collection Strategy @@ -150,7 +167,7 @@ of the first command's active span. The `ProfilingManager` self-profiles its own `span.__enter__` and `span.__exit__` calls. Exposes: -- `overhead_per_span_ns` — average cost of one span boundary in nanoseconds +- `overhead_per_span_us` — average cost of one span boundary in microseconds - `total_overhead_ms` — estimated total overhead across all active spans Visible in the UI to verify the profiler does not bias measurements significantly. @@ -186,19 +203,20 @@ CumulativeSpan --- -## Existing Code Hooks +## Code Hooks -### `src/myfasthtml/core/utils.py` — route handler (Level A) +### `src/myfasthtml/core/utils.py` — route handler (Level A) ✅ ```python -@utils_rt(Routes.Commands) -async def post(session, c_id: str, client_response: dict = None): - with profiler.span("command", args={"c_id": c_id}): - command = CommandsManager.get_command(c_id) - return await command.execute(client_response) +command = CommandsManager.get_command(c_id) +if command: + with profiler.command_span(command.name, c_id, client_response or {}): + return command.execute(client_response) ``` -### `src/myfasthtml/core/commands.py` — execution phases (Level B) +### `src/myfasthtml/core/commands.py` — execution phases (Level B) ⏳ Deferred + +Planned breakdown inside `Command.execute()`: ```python def execute(self, client_response=None): @@ -212,54 +230,120 @@ def execute(self, client_response=None): ... ``` +Deferred: will be added once the UI control is functional to immediately observe the breakdown. + +--- + +## UI Control Design + +### Control name: `Profiler` (SingleInstance) + +Single entry point. Replaces the earlier `ProfilerList` name. + +**Files:** +- `src/myfasthtml/controls/Profiler.py` +- `src/myfasthtml/assets/core/profiler.css` + +### Layout + +Split view using `Panel`: + +``` +┌─────────────────────────────────────────────────────┐ +│ [●] [🗑] Overhead/span: 1.2µs Traces: 8/500│ ← toolbar (icon-only) +├──────────────────────┬──────────────────────────────┤ +│ Command Duration Time│ NavigateCell — 173.4ms [≡][◕]│ +│ ──────────────────────│ ─────────────────────────────│ +│ NavigateCell 173ms … │ [Metadata card] │ +│ NavigateCell 168ms … │ [kwargs card] │ +│ SelectRow 42ms … │ [Span breakdown / Pie chart] │ +│ … │ │ +└──────────────────────┴──────────────────────────────┘ +``` + +### Toolbar + +Icon-only buttons, no `Menu` control (Menu does not support toggle state). +Direct `mk.icon()` calls: + +- **Enable/disable**: icon changes between "recording" and "stopped" states based on `profiler.enabled` +- **Clear**: delete icon, always red +- **Refresh**: manual refresh of the trace list (no auto-refresh yet — added in Step 2.1) + +Overhead metrics displayed as plain text on the right side of the toolbar. + +### Trace list (left panel) + +Three columns: command name / duration (color-coded) / timestamp. +Click on a row → update right panel via HTMX. + +**Duration color thresholds:** +- Green (`mf-profiler-fast`): < 20 ms +- Orange (`mf-profiler-medium`): 20–100 ms +- Red (`mf-profiler-slow`): > 100 ms + +### Detail panel (right) + +Two view modes, toggled by icons in the detail panel header: + +1. **Tree view** (default): Properties-style cards (Metadata, kwargs) + span breakdown with + proportional bars and indentation. Cumulative spans show `×N · min/avg/max` badge. +2. **Pie view**: `ProfilerPieChart` control (future) — distribution of time across spans + at the current zoom level. + +The `Properties` control is used as-is for Metadata and kwargs cards. +The span breakdown is custom rendering (not a `Properties` instance). + +### Font conventions + +- Labels, headings, command names: `--font-sans` (DaisyUI default) +- Values (durations, timestamps, kwargs values): `--font-mono` +- Consistent with `properties.css` (`mf-properties-value` uses `--default-mono-font-family`) + +### Visual reference + +Mockups available in `examples/`: +- `profiler_mockup.html` — first iteration (monospace font everywhere) +- `profiler_mockup_2.html` — **reference** (correct fonts, icon toolbar, tree/pie toggle) + --- ## Implementation Plan -### Phase 1 — Core +### Phase 1 — Core ✅ Complete -**File**: `src/myfasthtml/core/profiler.py` +1. `ProfilingSpan`, `CumulativeSpan`, `ProfilingTrace` dataclasses +2. `ProfilingManager` with all probe mechanisms +3. `profiler` singleton +4. Hook into `utils.py` (Level A) ✅ +5. Hook into `commands.py` (Level B) — deferred -1. `ProfilingSpan` dataclass -2. `CumulativeSpan` dataclass -3. `ProfilingTrace` dataclass -4. `ProfilingManager` class with all probe mechanisms -5. `profiler` singleton -6. Hook into `utils.py` (Level A) -7. Hook into `commands.py` (Level B) - -**Tests**: `tests/core/test_profiler.py` - -| Test | Description | -|------|-------------| -| `test_i_can_create_a_span` | Basic span creation and timing | -| `test_i_can_nest_spans` | Child spans are correctly parented | -| `test_i_can_use_span_as_decorator` | Decorator captures args automatically | -| `test_i_can_use_cumulative_span` | Aggregates count/total/min/max/avg | -| `test_i_can_attach_data_to_span` | `span.set()` and `current_span().set()` | -| `test_i_can_clear_traces` | Buffer is emptied after `clear()` | -| `test_i_can_enable_disable_profiler` | Probes are no-ops when disabled | -| `test_i_can_measure_overhead` | Overhead metrics are exposed | -| `test_i_can_use_trace_all_on_class` | All methods of a class are wrapped | -| `test_i_can_use_trace_calls_on_function` | Sub-calls are traced via setprofile | +**Tests**: `tests/core/test_profiler.py` — 7 classes, full coverage ✅ ### Phase 2 — Controls -**`src/myfasthtml/controls/ProfilerList.py`** (SingleInstance) -- Table of all traces: command name / total duration / timestamp -- Right panel: trace detail (kwargs, span breakdown) -- Buttons: enable/disable, clear -- Click on a trace → opens ProfilerDetail +#### Step 2.1 — Global layout (current) 🔄 -**`src/myfasthtml/controls/ProfilerDetail.py`** (MultipleInstance) -- Hierarchical span tree for a single trace -- Two display modes: list and pie chart -- Click on a span → zooms into its children (if any) -- Displays cumulative spans with count/min/max/avg -- Shows overhead metrics +`src/myfasthtml/controls/Profiler.py`: +- `SingleInstance` inheriting +- Toolbar: `mk.icon()` for enable/disable and clear, overhead text +- `Panel` for split layout +- Left: trace list table (command / duration / timestamp), click → select_trace command +- Right: placeholder (empty until Step 2.2) -**`src/myfasthtml/controls/ProfilerPieChart.py`** (future) -- Pie chart visualization of span distribution at a given zoom level +`src/myfasthtml/assets/core/profiler.css`: +- All `mf-profiler-*` classes + +#### Step 2.2 — Detail panel ⏳ + +Right panel content: +- Metadata and kwargs via `Properties` +- Span tree: custom `_mk_span_tree()` with bars and cumulative badges +- View toggle (tree / pie) in detail header + +#### Step 2.3 — Pie chart ⏳ Future + +`src/myfasthtml/controls/ProfilerPieChart.py` --- diff --git a/examples/profiler_mockup.html b/examples/profiler_mockup.html new file mode 100644 index 0000000..2e878f0 --- /dev/null +++ b/examples/profiler_mockup.html @@ -0,0 +1,640 @@ + + +
+ +