Metadata-Version: 2.4
Name: fxprofiler
Version: 0.1.0
Summary: Create marker-only performance profiles in the Firefox Profiler.
License-Expression: MPL-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# fxprofiler

Create marker-only performance profiles in the Firefox Profiler. The [profiling](https://docs.python.org/3/library/profiling.html)
module allows for sampling based profiling, but no markers. Unfortunately it's not possible
yet to do both sampling and markers in Python together. This package is designed for
manually instrumenting python code with deterministic tracing markers or spans.

## Define a marker type

A simple Text marker can be created to create an Instant marker at a specific point
in time.

```python
from dataclasses import dataclass
from fxprofiler import Marker, MarkerFormat, marker_field


@dataclass
class TextMarker(Marker):
    type_name = "Text"
    chart_label = "{marker.data.text}"
    table_label = "{marker.name} - {marker.data.text}"

    text: str = marker_field(label="Contents", format=MarkerFormat.UNIQUE_STRING)
```

Additional information can be collected through the `marker_field` mechannism.

```python
from dataclasses import dataclass
from fxprofiler import (
    Category,
    CategoryColor,
    Marker,
    MarkerFormat,
    MarkerGraph,
    MarkerGraphType,
    marker_field,
)


@dataclass
class AllocationMarker(Marker):
    type_name = "custom"
    category = Category("Graphics", CategoryColor.GREEN)
    description = "Tracks a single allocation event."
    tooltip_label = "Allocation - {marker.data.url}"
    graphs = (MarkerGraph(key="latency_ms", type=MarkerGraphType.LINE),)

    event_name: str = marker_field(
        label="Event name", format=MarkerFormat.UNIQUE_STRING
    )
    allocation_size: int = marker_field(
        label="Allocation size", format=MarkerFormat.BYTES
    )
    url: str = marker_field(label="URL", format=MarkerFormat.URL)
    latency_ms: float = marker_field(label="Latency", format=MarkerFormat.DURATION)
```

## Building a profile

In order to add markers, a full profile must be built. The profiles follow a thread and
process model. Even if your system has a different abstraction, you can fit your data
into these groupings. Threads are grouped by pid in the profiler UI. You can create
synthetic PIDs to group threads.

```python
from fxprofiler import Profile, MarkerTiming

profile = Profile(product="My Script", start_time_ms_since_epoch=0.0)
thread = profile.add_thread("Main Thread", pid=1, tid=1, is_main=True)

profile.add_marker(
    thread, "startup", MarkerTiming.instant(0.0), TextMarker(text="App launched")
)
profile.add_marker(
    thread,
    "allocate",
    MarkerTiming.interval(5.0, 42.0),
    AllocationMarker(
        event_name="alloc",
        allocation_size=512_000,
        url="https://example.com/",
        latency_ms=12.3,
    ),
)
```

## Context managers for timing

`measure()` records an interval marker for the duration of the context block.

```python
with profile.measure(thread, "render", TextMarker(text="Rendered frame")):
    render_frame()
```

`thread()` adds a thread that is alive for the duration of the context block.

```python
import threading


def worker(profile):
    with profile.thread("Worker") as thread:
        with profile.measure(thread, "task", TextMarker(text="task")):
            do_work()


threading.Thread(target=worker, args=(profile,)).start()
```

## Interval-start / interval-end pairs

If markers involve a duration, the API supports adding them with two different markers.

```python
profile.add_marker(
    thread, "fetch", MarkerTiming.interval_start(2.0), TextMarker(text="fetch started")
)
# ... later, possibly after the end time wasn't known yet when the marker started ...
profile.add_marker(
    thread, "fetch", MarkerTiming.interval_end(30.0), TextMarker(text="fetch finished")
)
```

## Writing and opening a profile

When integrating into your app, the profile can be saved and then served directly.

```python
profile.write_json("profile.json")
```

```python
from fxprofiler import show

server = show(profile)
input("Press Enter to stop serving...\n")
server.shutdown()
```

## Examples

Runnable scripts that open profiles live in `examples/`:

```sh
task example -- markers
```

## Development

All of the tasks can be run through [Taskfile](https://taskfile.dev/).

## Releasing

Describe the release under `## Unreleased` in `CHANGELOG.md`, then run:

```sh
task publish -- patch   # or minor / major
```
