> For the complete documentation index, see [llms.txt](https://triton.pharos.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://triton.pharos.ai/common-apex-usage-patters/performance-marks.md).

# Performance Marks

Time any stretch of Apex with startMark / endMark — names derived from the stack trace, automatic nesting, and every log inside the mark parented to it.

Timing a piece of Apex used to mean a stopwatch you wrote yourself: a `Long` before, a subtraction after, and a `.duration(...)` on the log. **Marks** replace that with two calls that also build a span tree — and they don't ask you to type a name.

{% code lineNumbers="true" %}

```apex
public void recalculateForecast() {
    Triton.startMark();
    try {
        // ... the work being measured ...
    } finally {
        Triton.endMark();
    }
}
```

{% endcode %}

That's the whole idiom. The mark is named `ForecastService.recalculateForecast` — derived from the stack trace Triton already captures — and it emits a pair of records: one when it opens, one when it closes carrying the elapsed milliseconds.

This is the Apex counterpart of the LWC [Custom Performance Marks](/beyond-apex/profiling/custom-marks.md). Same concept, same record shape, so a trace that starts in the browser and lands in Apex reads as one tree.

## Naming: derived by default

`startMark()` takes no arguments on purpose. The class and method are already in the stack trace, so making you retype them just creates strings that drift — a renamed method leaves a mark (and any `Log_Level__mdt` rule pointing at it) quietly stale. A derived name cannot drift.

It returns the derived name, which is handy when you need to close out of order:

{% code lineNumbers="true" %}

```apex
String mark = Triton.startMark();     // → 'ForecastService.recalculateForecast'
```

{% endcode %}

When the meaningful name simply *isn't* a method name, supply one:

{% code lineNumbers="true" %}

```apex
Triton.startMark('nightly-reconcile');
Triton.startMark('chunk-3-of-40');
```

{% endcode %}

Use the explicit form for a name the framework can't know, or when you want to measure one method in several labelled segments.

{% hint style="info" %}
**`startMark(String)` is not a performance escape hatch.** Both forms capture the stack once — the records need a trace regardless — so supplying a name skips only a short string parse, not the capture. Pick whichever reads better; they cost effectively the same. If derivation can't produce a name, the mark falls back to `AnonymousMark` plus a counter, so it still nests and is still identifiable.
{% endhint %}

## Nesting

Marks form a stack. `endMark()` closes the innermost open mark, whatever it's called — which is why the `finally` block never has to name anything, and can't silently no-op on a typo:

{% code lineNumbers="true" %}

```apex
Triton.startMark();                      // OrderService.submit
try {
    Triton.startMark('validate');        // nested inside OrderService.submit
    try {
        validate(orders);
    } finally {
        Triton.endMark();                // closes 'validate'
    }

    persist(orders);
} finally {
    Triton.endMark();                    // closes OrderService.submit
}
```

{% endcode %}

For work that legitimately finishes out of order — opened in one method, closed in another — close by name with `endMark(String)`; it closes the newest open mark carrying that name.

`clearMarks()` discards every open mark and resets to the request root. Call it when you want that reset explicitly — `startTransaction()` does **not** clear marks while one is open, and you do **not** need `clearMarks()` at the top of a batch chunk or a queueable (see [Async and batch](#async-and-batch)). It's there for deliberate recovery to a known-good state.

## Trigger handlers

`startTriggerMark()` is the one shorthand, and it exists for one reason: `Trigger.operationType` is the single piece of naming information a stack trace *cannot* carry. `BEFORE_UPDATE` and `AFTER_UPDATE` run the same method from the same frame, so without it a trigger cascade collapses into one indistinguishable span.

{% code lineNumbers="true" %}

```apex
public void run() {
    Triton.startTriggerMark();     // → 'OpportunityHandler.BEFORE_UPDATE'
    try {
        dispatch();
    } finally {
        Triton.endMark();
    }
}
```

{% endcode %}

Zero arguments — the handler class comes from the stack trace, the context from `Trigger.operationType`. Put it in your base handler's dispatch method and every handler in the org is instrumented without touching one of them. Called outside trigger context, there's no operation to append, so it produces the same `<Class>.<method>` name `startMark()` would — it is exactly `startMark()` at that point, safe to leave in a method that's also reachable outside a trigger.

{% hint style="info" %}
**There is no boundary method and no batch method.** A request boundary is `startMark()` at the top of the endpoint; a batch phase is `startMark()` at the top of `execute()`. Depth is a fact the framework already knows, not something you declare — the outermost mark parents to the transaction automatically.
{% endhint %}

## What gets logged

A mark emits **two** records — one on open, one on close — so a transaction that dies mid-mark still shows the work having started.

| Field               | Opening record                | Completion record                           |
| ------------------- | ----------------------------- | ------------------------------------------- |
| Summary             | `Performance started: <mark>` | `Performance: <mark>`                       |
| Details             | *(unset)*                     | `Duration: <n>ms`                           |
| Type                | `Performance`                 | `Performance`                               |
| Action              | `<mark>`                      | `<mark>`                                    |
| Duration            | *(unset)*                     | the elapsed milliseconds                    |
| `Span_Id__c`        | the mark's span               | the **same** span                           |
| `Parent_Span_Id__c` | the enclosing span            | the **same** enclosing span                 |
| Stack trace         | the `startMark()` call site   | the **same** trace, not `endMark()`'s frame |
| Level               | `FINE`                        | `FINE`                                      |
| Category / Area     | inherited from `Triton.t`     | inherited from `Triton.t`                   |

Both halves are built from your template, so an entry point that sets an Area gets it on its marks for free. Both carry the *opening* call site's stack trace: they describe the same span, and where the two genuinely differ — an out-of-order close from another method — the opening frame is the more useful one, because that's where the measured work began.

The opening record omits governor-limit info (it has no duration and no deltas worth having); the completion record keeps it.

### Levels are a rule, not an argument

Marks always log at `FINE`. There is deliberately **no** `level` parameter, because a level baked into a call site overrides the `Log_Level__mdt` rule an admin is supposed to control without a deploy — the opposite of how the rest of Triton works. To turn marks on for one class, write the rule:

| Field     | Value             |
| --------- | ----------------- |
| Component | `ForecastService` |
| Level     | `FINE`            |

Every other field left blank is a wildcard, so that record targets `ForecastService` logs in any category, type, or area. See [Log Levels (resolution rules)](/core-concepts/log-levels.md) for how competing rules resolve.

## Logs inside a mark become children of it

This is the part that turns marks from a stopwatch into a trace. While a mark is open, every ordinary log you emit is parented to it:

{% code lineNumbers="true" %}

```apex
Triton.startMark();                                     // OrderService.submit
try {
    Triton.info(Triton.t.summary('Validating 42 orders'));   // ← child of the mark
    Triton.fine(Triton.t.summary('Pricing applied'));        // ← child of the mark
} finally {
    Triton.endMark();
}
```

{% endcode %}

You write nothing extra. The outermost mark parents to the transaction span; a nested mark parents to the mark enclosing it; ordinary logs become leaves under whichever mark is innermost. Full model in [Spans & Distributed Tracing](/end-to-end/spans-and-tracing.md).

{% hint style="info" %}
**An id you set yourself always wins.** If you call `.spanId(...)` or `.parentSpanId(...)` on a builder, marks never overwrite it. That's what keeps an LWC → Apex trace intact: the ids the browser forwarded stay exactly as they arrived.
{% endhint %}

### Filtered-out marks stay transparent

If a `Log_Level__mdt` rule rejects a mark's opening record, the mark doesn't leave a hole behind it. Its children re-parent to the nearest surviving ancestor instead of pointing at a span that was never published — so dialling a class down thins the tree rather than orphaning it.

## Marks never flush

`startMark()` and `endMark()` use the buffered path, exactly like `Triton.log()`. No mark method ever publishes a platform event, so a mark opened and closed inside a loop adds two buffered records per iteration and nothing else. Your existing flush points stay yours.

{% hint style="warning" %}
**Buffered means buffered.** Paired records make a crash *after* a flush visible — the opening record is already out. They don't rescue a buffer that never flushed. If you can't afford to lose a boundary event, publish it with `Triton.logNow(...)` as you would today.
{% endhint %}

## Async and batch

Apex statics don't survive a batch chunk, a `Queueable`, or a `@future`, and the mark stack is a static. That's fine — each async unit starts clean and becomes its own subtree under the shared transaction, which is what you want anyway.

The whole batch idiom is two calls you already have:

{% code lineNumbers="true" %}

```apex
public void execute(Database.BatchableContext bc, List<Lead> leads) {
    Triton.resumeTransaction(transactionId);   // if you carry one (Database.Stateful)
    Triton.startMark();                        // → 'LeadsBatch.execute'
    try {
        // ... process the chunk ...
    } finally {
        Triton.endMark();
        Triton.flush();
    }
}
```

{% endcode %}

The phase *is* the method name, so `startMark()` derives `LeadsBatch.execute` for free — there's no batch-specific method to learn. `clearMarks()` isn't needed here: a fresh execution context already starts with an empty stack. Carrying the transaction id across chunks is still `Database.Stateful` plus an instance field, unchanged — see [Batch Logging](/common-apex-usage-patters/batch-logging.md).

## Opting in

Marks change nothing until you add one. A transaction with no open mark produces byte-identical span fields to before.

{% hint style="warning" %}
**Once a mark is open, span fields change shape.** `Span_Id__c` on an ordinary log inside a mark becomes a fresh UUID instead of the request id. If a query, report, or dashboard treats `Span_Id__c == requestId` as "this log belongs to request X", move it to **`pharos__Request_Id_External__c`**, which is where that fact actually lives. `Parent_Span_Id__c` also goes from always-null to populated, and log volume grows by two records per mark.

A mark that log levels filtered out is still an *open* mark, so it still reshapes the logs around it — suppression is transparent for parenting, not for data shape.
{% endhint %}

## Do / Don't

{% hint style="success" %}
**Do**

* **Prefer `Triton.startMark()` with no arguments.** The derived name can't drift out of sync with a rename.
* **Close in a `finally`.** An exception mid-mark shouldn't leave the stack open.
* **Use `startTriggerMark()` in your base trigger handler** — one call instruments every handler context in the org.
* **Reach for `startMark(String)` when the name isn't a method name**, not as a performance optimisation.
* **Control verbosity with `Log_Level__mdt`**, the same way you do for every other log.
  {% endhint %}

{% hint style="danger" %}
**Don't**

* **Don't hand-time work you've marked.** The completion record already carries the duration; a manual `System.now().getTime()` alongside it is duplication. Manual timing is still fine for a one-off you don't want a span for.
* **Don't flush inside a mark just to "get it out".** Marks are buffered by design; flush at your boundaries.
* **Don't call `clearMarks()` at the top of a batch chunk or queueable.** The stack is already empty there.
* **Don't assume `Span_Id__c` is the request id** once marks are in play.
  {% endhint %}

## Related reading

* [Spans & Distributed Tracing](/end-to-end/spans-and-tracing.md) — how the mark stack becomes a trace tree.
* [Custom Performance Marks](/beyond-apex/profiling/custom-marks.md) — the LWC equivalent.
* [Log Levels in Apex](/common-apex-usage-patters/log-levels.md) — dialling mark verbosity per environment.
* [`Triton` methods reference](/methods-reference/apex-methods-reference/triton.md#performance-marks) — signatures for all six methods.
