# scope_profiler.concurrency

*module*

Thread, asyncio-task and greenlet identity for the region profiler.

A region records a ``(start, end)`` pair and nothing else, which is enough
only while one call stack owns the process. With several threads - or several
coroutines interleaved on one thread - the recorded intervals of two lanes
overlap without nesting, and the whole downstream stack (flame chart,
exclusive time, ``.prof`` export) has no call graph to reconstruct. See
:class:`~scope_profiler.call_stack.NestingError`.

This module supplies the missing axis: every recorded call is stamped with the
*lane* it ran on, and the lanes themselves are described in two small tables
that travel with the run.

**Threads.** :class:`ThreadRecord` carries a dense per-rank index, the OS
identifiers, the thread's name, when it started and ended, and the CPU time it
burned. The index is what the event column stores; everything else is looked
up once, per thread, not per call.

**Tasks.** An asyncio task is a lane of its own: it runs on a thread, but so
do the other tasks that interleave with it, and a ``with`` block held across
an ``await`` covers wall time the task did not spend running. Instrumenting
the task - rather than the loop - gives both the identity and the split:
:class:`TaskRecord` accumulates running and suspended nanoseconds, and a
region reads the suspended counter at entry and exit to report the await time
of that one call. Greenlets are the same shape (cooperative lanes on one
thread) and share :class:`TaskRecord`, tagged ``kind="greenlet"``.

Nothing here is installed unless ``track_threads`` is set: the profiler's
default single-lane path is untouched, hot loop included.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L1-L1)

## scope_profiler.concurrency.CPU_SAMPLE_MASK

*attribute* · *module attribute*

```python
CPU_SAMPLE_MASK = 255
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L61-L61)

## scope_profiler.concurrency.NO_TASK

*attribute* · *module attribute*

```python
NO_TASK = -1
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L49-L49)

## scope_profiler.concurrency.UNKNOWN

*attribute* · *module attribute*

```python
UNKNOWN = -1
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L54-L54)

## scope_profiler.concurrency.ConcurrencyTracker

*class*

```python
class ConcurrencyTracker
```

Per-run registry of threads, asyncio tasks and greenlets.

One tracker belongs to one :class:`~scope_profiler.profile_config.ProfilingConfig`,
so two simultaneously active managers never share lane numbering. Creating
it is free; :meth:`install` is what puts the (few, cheap) hooks in place,
and :meth:`uninstall` takes them out again.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L327-L685)

### scope_profiler.concurrency.ConcurrencyTracker.track_async

*attribute* · *instance attribute*

```python
track_async = bool(track_async)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L337-L337)

### scope_profiler.concurrency.ConcurrencyTracker.tasks

*property*

```python
tasks: list[TaskRecord]
```

Every instrumented task and greenlet, in creation order.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L389-L392)

### scope_profiler.concurrency.ConcurrencyTracker.threads

*property*

```python
threads: list[ThreadRecord]
```

Every thread that recorded a call, in registration order.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L383-L386)

### scope_profiler.concurrency.ConcurrencyTracker.current_thread

*method*

```python
def current_thread() -> ThreadRecord
```

This thread's record, registering it on first use.

On the region hot path. The common case is one failed-then-cached
attribute lookup on a :class:`threading.local`, which is why the
registry itself is never consulted here.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L370-L380)

### scope_profiler.concurrency.ConcurrencyTracker.install

*method*

```python
def install() -> None
```

Put the thread-start, asyncio and greenlet hooks in place.

Idempotent, so a second ``setup()`` on the same configuration does not
stack wrappers.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L508-L521)

### scope_profiler.concurrency.ConcurrencyTracker.instrument_loop

*method*

```python
def instrument_loop(loop) -> None
```

Route ``loop``'s task creation through the step timer.

Public because a loop implementation that never reaches
``BaseEventLoop.run_forever`` (uvloop, say) can be handed here
directly. Chains onto whatever task factory the application already
installed instead of replacing it.

Instrumented once per loop, and keyed on the loop rather than on
whichever factory is currently in place: an application that installs
its own factory *after* the loop has started displaces ours, and
re-wrapping to get back on top would make the two call each other
forever, since its captured "previous" is us. Such a loop keeps
recording regions and thread ids; the tasks created after the swap
simply have no lane of their own.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L605-L624)

### scope_profiler.concurrency.ConcurrencyTracker.snapshot

*method*

```python
def snapshot(origin_ns: int = 0) -> dict
```

The lane tables, as columns, ready to be written or returned.

Timestamps are shifted by ``origin_ns`` (the run's start), matching
what the event columns store, so a thread's lifetime and the calls on
it share one timeline. :data:`UNKNOWN` entries are left alone.

Returns
-------
dict
    ``{"threads": {...}, "tasks": {...}}``, each a dict of equal-length
    numpy arrays. Empty arrays when nothing was tracked.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L629-L685)

### scope_profiler.concurrency.ConcurrencyTracker.uninstall

*method*

```python
def uninstall() -> None
```

Remove every hook and restore what was there before.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L569-L577)

## scope_profiler.concurrency.TaskInfo

*class*

```python
class TaskInfo
```

One row of a run's task table: an asyncio task or a greenlet.

``running_time`` and ``awaiting_time`` split the lane's wall time into the
part it held its thread and the part it did not, which is the number
``await``-heavy code is usually looking for.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L779-L844)

### scope_profiler.concurrency.TaskInfo.awaiting_time

*attribute* · *instance attribute*

```python
awaiting_time = float(suspended_ns) / 1000000000.0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L825-L825)

### scope_profiler.concurrency.TaskInfo.coro_name

*attribute* · *instance attribute*

```python
coro_name = str(coro_name)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L819-L819)

### scope_profiler.concurrency.TaskInfo.created_time

*attribute* · *instance attribute*

```python
created_time = float(created_ns) / 1000000000.0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L821-L821)

### scope_profiler.concurrency.TaskInfo.done_time

*attribute* · *instance attribute*

```python
done_time = None if int(done_ns) == UNKNOWN else float(done_ns) / 1000000000.0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L822-L822)

### scope_profiler.concurrency.TaskInfo.index

*attribute* · *instance attribute*

```python
index = int(index)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L815-L815)

### scope_profiler.concurrency.TaskInfo.kind

*attribute* · *instance attribute*

```python
kind = str(kind)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L817-L817)

### scope_profiler.concurrency.TaskInfo.name

*attribute* · *instance attribute*

```python
name = str(name)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L818-L818)

### scope_profiler.concurrency.TaskInfo.rank

*attribute* · *instance attribute*

```python
rank = int(rank)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L816-L816)

### scope_profiler.concurrency.TaskInfo.running_time

*attribute* · *instance attribute*

```python
running_time = float(running_ns) / 1000000000.0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L824-L824)

### scope_profiler.concurrency.TaskInfo.steps

*attribute* · *instance attribute*

```python
steps = int(steps)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L823-L823)

### scope_profiler.concurrency.TaskInfo.thread_index

*attribute* · *instance attribute*

```python
thread_index = int(thread_index)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L820-L820)

### scope_profiler.concurrency.TaskInfo.done

*property*

```python
done: bool
```

Whether the task finished before the run was collected.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L828-L830)

### scope_profiler.concurrency.TaskInfo.wall_time

*property*

```python
wall_time: float | None
```

Seconds from creation to completion, or None if it never finished.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L833-L837)

## scope_profiler.concurrency.TaskRecord

*class*

```python
class TaskRecord
```

One asyncio task or greenlet, and how its wall time split.

``running_ns`` is the time the lane actually held its thread, summed over
every step; ``suspended_ns`` is the time between steps - awaiting, or
switched away from. A region entered inside the lane reads
``suspended_ns`` on the way in and out, and the difference is that one
call's await time.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L64-L143)

### scope_profiler.concurrency.TaskRecord.coro_name

*attribute* · *instance attribute*

```python
coro_name = coro_name
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L100-L100)

### scope_profiler.concurrency.TaskRecord.created_ns

*attribute* · *instance attribute*

```python
created_ns = perf_counter_ns()
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L102-L102)

### scope_profiler.concurrency.TaskRecord.done_ns

*attribute* · *instance attribute*

```python
done_ns = UNKNOWN
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L103-L103)

### scope_profiler.concurrency.TaskRecord.index

*attribute* · *instance attribute*

```python
index = index
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L97-L97)

### scope_profiler.concurrency.TaskRecord.kind

*attribute* · *instance attribute*

```python
kind = kind
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L98-L98)

### scope_profiler.concurrency.TaskRecord.last_step_end_ns

*attribute* · *instance attribute*

```python
last_step_end_ns = 0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L110-L110)

### scope_profiler.concurrency.TaskRecord.name

*attribute* · *instance attribute*

```python
name = name
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L99-L99)

### scope_profiler.concurrency.TaskRecord.running_ns

*attribute* · *instance attribute*

```python
running_ns = 0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L105-L105)

### scope_profiler.concurrency.TaskRecord.step_started_ns

*attribute* · *instance attribute*

```python
step_started_ns = 0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L112-L112)

### scope_profiler.concurrency.TaskRecord.steps

*attribute* · *instance attribute*

```python
steps = 0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L104-L104)

### scope_profiler.concurrency.TaskRecord.suspended_ns

*attribute* · *instance attribute*

```python
suspended_ns = 0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L106-L106)

### scope_profiler.concurrency.TaskRecord.thread_index

*attribute* · *instance attribute*

```python
thread_index = thread_index
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L101-L101)

### scope_profiler.concurrency.TaskRecord.done

*property*

```python
done: bool
```

Whether the task finished before the run was collected.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L115-L117)

### scope_profiler.concurrency.TaskRecord.enter_step

*method*

```python
def enter_step() -> int
```

Charge the gap since the last step as suspension; return the clock.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L119-L126)

### scope_profiler.concurrency.TaskRecord.finish

*method*

```python
def finish() -> None
```

Record that the lane completed (first call wins).

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L134-L137)

### scope_profiler.concurrency.TaskRecord.leave_step

*method*

```python
def leave_step() -> None
```

Charge the step that is ending as running time, and suspend.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L128-L132)

## scope_profiler.concurrency.ThreadInfo

*class*

```python
class ThreadInfo
```

One row of a run's thread table, as post-processing sees it.

The read-back counterpart of :class:`ThreadRecord`: same fields, but
durations in seconds and timestamps relative to the start of the run,
matching every other time reported by
:class:`~scope_profiler.results.ProfilingResults`.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L717-L776)

### scope_profiler.concurrency.ThreadInfo.cpu_time

*attribute* · *instance attribute*

```python
cpu_time = float(cpu_ns) / 1000000000.0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L758-L758)

### scope_profiler.concurrency.ThreadInfo.daemon

*attribute* · *instance attribute*

```python
daemon = bool(daemon)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L755-L755)

### scope_profiler.concurrency.ThreadInfo.end_time

*attribute* · *instance attribute*

```python
end_time = None if int(end_ns) == UNKNOWN else float(end_ns) / 1000000000.0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L757-L757)

### scope_profiler.concurrency.ThreadInfo.ident

*attribute* · *instance attribute*

```python
ident = int(ident)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L753-L753)

### scope_profiler.concurrency.ThreadInfo.index

*attribute* · *instance attribute*

```python
index = int(index)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L750-L750)

### scope_profiler.concurrency.ThreadInfo.name

*attribute* · *instance attribute*

```python
name = str(name)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L752-L752)

### scope_profiler.concurrency.ThreadInfo.native_id

*attribute* · *instance attribute*

```python
native_id = int(native_id)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L754-L754)

### scope_profiler.concurrency.ThreadInfo.rank

*attribute* · *instance attribute*

```python
rank = int(rank)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L751-L751)

### scope_profiler.concurrency.ThreadInfo.start_time

*attribute* · *instance attribute*

```python
start_time = float(start_ns) / 1000000000.0
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L756-L756)

### scope_profiler.concurrency.ThreadInfo.alive

*property*

```python
alive: bool
```

Whether the thread was still running when the run was collected.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L761-L763)

### scope_profiler.concurrency.ThreadInfo.wall_time

*property*

```python
wall_time: float | None
```

Seconds between the thread starting and ending, or None if alive.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L766-L770)

## scope_profiler.concurrency.ThreadRecord

*class*

```python
class ThreadRecord
```

One thread of the profiled process, as the lane tables describe it.

``index`` is dense within a rank and is what the per-call ``thread_ids``
column stores. ``cpu_ns`` is exact for a thread that has ended and a
sampled lower bound for one still running; see :data:`CPU_SAMPLE_MASK`.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L146-L195)

### scope_profiler.concurrency.ThreadRecord.cpu_ns

*attribute* · *instance attribute*

```python
cpu_ns = thread_time_ns()
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L174-L174)

### scope_profiler.concurrency.ThreadRecord.daemon

*attribute* · *instance attribute*

```python
daemon = bool(thread.daemon)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L171-L171)

### scope_profiler.concurrency.ThreadRecord.end_ns

*attribute* · *instance attribute*

```python
end_ns = UNKNOWN
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L173-L173)

### scope_profiler.concurrency.ThreadRecord.ident

*attribute* · *instance attribute*

```python
ident = int(thread.ident or 0)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L168-L168)

### scope_profiler.concurrency.ThreadRecord.index

*attribute* · *instance attribute*

```python
index = index
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L167-L167)

### scope_profiler.concurrency.ThreadRecord.name

*attribute* · *instance attribute*

```python
name = str(thread.name)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L170-L170)

### scope_profiler.concurrency.ThreadRecord.native_id

*attribute* · *instance attribute*

```python
native_id = int(getattr(thread, 'native_id', 0) or 0)
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L169-L169)

### scope_profiler.concurrency.ThreadRecord.start_ns

*attribute* · *instance attribute*

```python
start_ns = perf_counter_ns()
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L172-L172)

### scope_profiler.concurrency.ThreadRecord.task

*attribute* · *instance attribute*

```python
task: TaskRecord | None = None
```

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L179-L179)

### scope_profiler.concurrency.ThreadRecord.alive

*property*

```python
alive: bool
```

Whether the thread was still running when the run was collected.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L182-L184)

### scope_profiler.concurrency.ThreadRecord.sample_cpu

*method*

```python
def sample_cpu() -> None
```

Refresh this thread's CPU time. Only valid on the thread itself.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L186-L188)

## scope_profiler.concurrency.lane_ids

*function*

```python
def lane_ids(thread_ids: np.ndarray, task_ids: np.ndarray | None) -> np.ndarray
```

One id per call identifying the stack it belongs to.

A call belongs to its task when it ran inside one and to its bare thread
otherwise. The two id spaces are disjoint by construction: thread lanes
are mapped to ``-2 - thread``, task lanes stay non-negative, and ``-1`` is
left for a call whose thread is unknown - a region recorded by the
Fortran API, say, folded into an otherwise thread-aware run. This is the
key :func:`~scope_profiler.call_stack.build_call_arrays` groups by, since
only calls sharing a lane can nest.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L698-L714)

## scope_profiler.concurrency.lane_tables_from_columns

*function*

```python
def lane_tables_from_columns(rank: int, tables: dict) -> tuple[list, list]
```

Turn one rank's stored lane columns into :class:`ThreadInfo`/:class:`TaskInfo`.

The one place the column layout written to HDF5 and returned in memory is
turned back into objects, so the file and ``finalize(return_results=True)``
cannot disagree about what a run's lanes were.

[View source](https://github.com/max-models/scope-profiler/blob/devel/src/scope_profiler/concurrency.py#L847-L891)
