Added Profiler control with basic UI
This commit is contained in:
176
docs/Profiler.md
176
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`
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user