reactor-runtime 3.7.0__tar.gz → 3.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/PKG-INFO +1 -1
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/pyproject.toml +1 -1
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/pyproject.toml.orig +1 -1
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/__init__.py +5 -1
- reactor_runtime-3.8.0/src/reactor_runtime/core/stats.py +55 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/transport.py +24 -1
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/values.py +5 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/app/reactor_app.py +8 -1
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/internal/reactor_core.py +20 -1
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/connection_manager.py +16 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/runner.py +85 -4
- reactor_runtime-3.8.0/src/reactor_runtime/runner/runtime_stats.py +204 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/connection.py +17 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/peer.py +50 -13
- reactor_runtime-3.8.0/src/reactor_runtime/transport/webrtc/reading.py +124 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/stats.py +23 -1
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/LICENSE +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/NOTICE +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/README.md +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/codes.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/fields.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/model.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/naming.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/service.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/session.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/core/typespec.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/distributed/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/distributed/errors.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/distributed/ipc.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/distributed/protocol.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/distributed/rank.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/distributed/runner.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/distributed/worker.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/event_stream.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/http/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/http/events.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/http/routes.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/http/server.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/http/spec.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/app/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/app/input_state.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/app/outcome.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/app/step_completed.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/client.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/events/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/events/decorators.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/events/errors.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/events/messages.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/internal/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/internal/aliases.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/internal/bridge.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/internal/input_buffer.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/model/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/model/contract.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/model/reactor_model.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/model/schema.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/pipeline/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/pipeline/idle.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/pipeline/input_state.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/pipeline/reactor_pipeline.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/tracks/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/tracks/descriptors.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/tracks/input.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/tracks/output.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/log.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/manifest.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/message_gateway.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/metrics/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/metrics/command.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/metrics/model.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/metrics/registry.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/metrics/session.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/paths.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/protocol/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/protocol/base.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/protocol/common.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/protocol/v0/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/protocol/v0/codec.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/protocol/v1/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/protocol/v1/codec.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/py.typed +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/recording/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/recording/chunk_encoder.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/recording/markers.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/recording/recorder.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/client_stats.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/offer_epochs.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/session_start.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/state_machine.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/system_client.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/upload_resolution.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/schema.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/serve.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/service.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/step_results/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/step_results/mp4.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/step_results/store.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/acceptor.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/router.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/__init__.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/acceptor.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/client_stats.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/config.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/frames.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/metrics.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/pacer.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/router.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/sdp.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/signaling.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/version.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/upload_store.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/common_pb2.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/common_pb2.pyi +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/control_pb2.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/control_pb2.pyi +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/data_pb2.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/data_pb2.pyi +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/model_pb2.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/model_pb2.pyi +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/platform_pb2.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/platform_pb2.pyi +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/track_pb2.py +0 -0
- {reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_wire/v1/track_pb2.pyi +0 -0
|
@@ -33,7 +33,8 @@ from reactor_runtime.core.session import (
|
|
|
33
33
|
SessionState,
|
|
34
34
|
Transition,
|
|
35
35
|
)
|
|
36
|
-
from reactor_runtime.core.
|
|
36
|
+
from reactor_runtime.core.stats import TransportReading, TransportTrackReading
|
|
37
|
+
from reactor_runtime.core.transport import Connection, ConnectionSink, TransportStatsSource
|
|
37
38
|
from reactor_runtime.core.typespec import TypeSpec
|
|
38
39
|
from reactor_runtime.core.values import (
|
|
39
40
|
ClientConnectionStat,
|
|
@@ -98,6 +99,9 @@ __all__ = [
|
|
|
98
99
|
"TrackKind",
|
|
99
100
|
"Transition",
|
|
100
101
|
"TransitionEvent",
|
|
102
|
+
"TransportReading",
|
|
103
|
+
"TransportStatsSource",
|
|
104
|
+
"TransportTrackReading",
|
|
101
105
|
"TypeSpec",
|
|
102
106
|
"UploadedFile",
|
|
103
107
|
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Readings a transport takes of its own wire.
|
|
2
|
+
|
|
3
|
+
A transport that measures its wire describes what it measured in these types,
|
|
4
|
+
which follow a client's own stats reading (:class:`~reactor_runtime.core.ClientTrackStat`):
|
|
5
|
+
what is fixed about a track stays a field, and the measurements go in an open
|
|
6
|
+
``metrics`` mapping. Each transport names what it measures, so a new transport
|
|
7
|
+
reports its own measurements without a change here, and a reader holds the same
|
|
8
|
+
shape whichever transport took the reading.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
from reactor_runtime.core.values import ClientTrackDirection, TrackKind
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TransportTrackReading:
|
|
21
|
+
"""What a transport measured of one track on one connection.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
track_name: The track the reading is for.
|
|
25
|
+
kind: The track's kind; ``None`` when the transport does not know it.
|
|
26
|
+
direction: The track's direction as the client sees it: a model
|
|
27
|
+
output track is one the client receives.
|
|
28
|
+
codec: The negotiated codec, e.g. ``"VP9"``, ``"opus"``; empty when
|
|
29
|
+
the transport has not reported one.
|
|
30
|
+
metrics: The measurements, by name. Names match a client reading's
|
|
31
|
+
where both measure the same thing (``"bitrate_bps"``,
|
|
32
|
+
``"packets_lost"``, ``"frames_per_second"``), and a measurement the
|
|
33
|
+
transport has no value for is left out rather than reported as
|
|
34
|
+
zero.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
track_name: str
|
|
38
|
+
kind: TrackKind | None
|
|
39
|
+
direction: ClientTrackDirection
|
|
40
|
+
codec: str = ""
|
|
41
|
+
metrics: Mapping[str, float] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class TransportReading:
|
|
46
|
+
"""What a transport measured of one connection's wire at one moment.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
metrics: Measurements of the connection as a whole, by name, e.g.
|
|
50
|
+
``"connection_rtt_ms"`` or ``"available_outgoing_bitrate_bps"``.
|
|
51
|
+
tracks: One reading per track the transport measured.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
metrics: Mapping[str, float] = field(default_factory=dict)
|
|
55
|
+
tracks: tuple[TransportTrackReading, ...] = ()
|
|
@@ -4,7 +4,8 @@ The two structural protocols that form the seam between a transport and the
|
|
|
4
4
|
runner, pointing opposite ways. ``Connection`` carries commands down to one
|
|
5
5
|
client; ``ConnectionSink`` carries facts up from the transport. Both are
|
|
6
6
|
``Protocol``s, so a transport author or a test fake conforms by shape and
|
|
7
|
-
signaling never reaches the runner.
|
|
7
|
+
signaling never reaches the runner. ``TransportStatsSource`` is an optional
|
|
8
|
+
capability a connection adds when its transport measures its wire.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
@@ -12,6 +13,7 @@ from __future__ import annotations
|
|
|
12
13
|
from collections.abc import Mapping
|
|
13
14
|
from typing import Protocol, runtime_checkable
|
|
14
15
|
|
|
16
|
+
from reactor_runtime.core.stats import TransportReading
|
|
15
17
|
from reactor_runtime.core.values import (
|
|
16
18
|
ClientStatsBatch,
|
|
17
19
|
ConnectionCapabilities,
|
|
@@ -95,6 +97,27 @@ class Connection(Protocol):
|
|
|
95
97
|
"""Tear the connection down."""
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
@runtime_checkable
|
|
101
|
+
class TransportStatsSource(Protocol):
|
|
102
|
+
"""A connection whose transport samples statistics about its own wire.
|
|
103
|
+
|
|
104
|
+
Optional, beside :class:`Connection`: a transport that measures its wire
|
|
105
|
+
implements it on its connections, and a reader checks for it with
|
|
106
|
+
``isinstance`` before reading. A connection without it is a connection
|
|
107
|
+
with nothing to report, not an error, so a transport that predates it
|
|
108
|
+
keeps working unchanged.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def latest_reading(self) -> TransportReading | None:
|
|
113
|
+
"""The most recent reading of this connection's wire.
|
|
114
|
+
|
|
115
|
+
``None`` before the transport has taken its first. The reading is
|
|
116
|
+
replaced as the transport takes new ones, so reading it never waits on
|
|
117
|
+
the wire.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
|
|
98
121
|
@runtime_checkable
|
|
99
122
|
class ConnectionSink(Protocol):
|
|
100
123
|
"""The transport-to-runner upward channel — the facts-up half.
|
|
@@ -410,12 +410,17 @@ class MediaChunk:
|
|
|
410
410
|
wait: Whether a consumer with a bounded queue should make the producer
|
|
411
411
|
wait for room (backpressure, throttling the model to the playout
|
|
412
412
|
rate) instead of dropping the overflow.
|
|
413
|
+
session: The number of the session the model was in when it emitted
|
|
414
|
+
the chunk, counted on the model's side, or ``None`` when the
|
|
415
|
+
emitter does not count sessions. A consumer compares it with its
|
|
416
|
+
own count to tell a late chunk of an earlier session apart.
|
|
413
417
|
"""
|
|
414
418
|
|
|
415
419
|
bundle: MediaBundle
|
|
416
420
|
fps: float
|
|
417
421
|
n_frames: int = 1
|
|
418
422
|
wait: bool = False
|
|
423
|
+
session: int | None = None
|
|
419
424
|
|
|
420
425
|
def frames(self) -> list[MediaBundle]:
|
|
421
426
|
"""Split the chunk into one single-frame bundle per carried frame."""
|
{reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/interface/app/reactor_app.py
RENAMED
|
@@ -74,6 +74,7 @@ from reactor_runtime.interface.events.decorators import (
|
|
|
74
74
|
from reactor_runtime.interface.events.errors import ApplicationError, CommandError
|
|
75
75
|
from reactor_runtime.interface.events.messages import ModelMessage
|
|
76
76
|
from reactor_runtime.interface.internal.reactor_core import (
|
|
77
|
+
STEP_SESSION,
|
|
77
78
|
CommandEnvelope,
|
|
78
79
|
ReactorCore,
|
|
79
80
|
RequestId,
|
|
@@ -353,7 +354,13 @@ class ReactorApp(ReactorCore):
|
|
|
353
354
|
rate = None
|
|
354
355
|
if report.output is not None:
|
|
355
356
|
pace = None if fps_pinned else outcome.elapsed
|
|
356
|
-
|
|
357
|
+
# The media is the step's, so it carries the
|
|
358
|
+
# session the step began in, as the report does.
|
|
359
|
+
token = STEP_SESSION.set(session)
|
|
360
|
+
try:
|
|
361
|
+
await self.emit(report.output, compute_time=pace)
|
|
362
|
+
finally:
|
|
363
|
+
STEP_SESSION.reset(token)
|
|
357
364
|
frames = self._to_bundle(report.output).frame_count
|
|
358
365
|
rate = self._playout_rate(frames, pace)
|
|
359
366
|
self._report_step(report, session, rate)
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import contextlib
|
|
15
|
+
import contextvars
|
|
15
16
|
import json
|
|
16
17
|
import logging
|
|
17
18
|
import threading
|
|
@@ -50,6 +51,17 @@ RequestId = str
|
|
|
50
51
|
|
|
51
52
|
_Holder = TypeVar("_Holder")
|
|
52
53
|
|
|
54
|
+
STEP_SESSION: contextvars.ContextVar[int | None] = contextvars.ContextVar(
|
|
55
|
+
"reactor_step_session", default=None
|
|
56
|
+
)
|
|
57
|
+
"""The session the step being emitted began in, set by the step loop around its emit.
|
|
58
|
+
|
|
59
|
+
A step reports the session it began in, and its media is stamped with the
|
|
60
|
+
same number, so both count toward one session even when the session changes
|
|
61
|
+
while the step emits. An emit made outside a step leaves it unset and is
|
|
62
|
+
stamped with the session the model is in when it emits.
|
|
63
|
+
"""
|
|
64
|
+
|
|
53
65
|
BroadcastSink = Callable[[ModelMessage], None]
|
|
54
66
|
|
|
55
67
|
AddressedSink = Callable[[ConnId, "ModelMessage | CommandFailure | None", RequestId | None], None]
|
|
@@ -167,7 +179,14 @@ class OutputStream:
|
|
|
167
179
|
fps = core._playout_rate(n_frames, compute_time)
|
|
168
180
|
core._last_emit_fps = fps
|
|
169
181
|
if core._out_media is not None:
|
|
170
|
-
|
|
182
|
+
session = STEP_SESSION.get()
|
|
183
|
+
chunk = MediaChunk(
|
|
184
|
+
bundle=bundle,
|
|
185
|
+
fps=fps,
|
|
186
|
+
n_frames=n_frames,
|
|
187
|
+
wait=not drop,
|
|
188
|
+
session=core._sessions_started if session is None else session,
|
|
189
|
+
)
|
|
171
190
|
await asyncio.to_thread(core._out_media, chunk)
|
|
172
191
|
await asyncio.sleep(0)
|
|
173
192
|
|
{reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/runner/connection_manager.py
RENAMED
|
@@ -48,6 +48,8 @@ from reactor_runtime.core import (
|
|
|
48
48
|
ConnId,
|
|
49
49
|
MediaChunk,
|
|
50
50
|
SessionEvent,
|
|
51
|
+
TransportReading,
|
|
52
|
+
TransportStatsSource,
|
|
51
53
|
)
|
|
52
54
|
from reactor_runtime.log import get_logger
|
|
53
55
|
from reactor_runtime.protocol import Channel, ProtocolVersion
|
|
@@ -139,6 +141,20 @@ class ConnectionManager:
|
|
|
139
141
|
"""The number of connections currently registered."""
|
|
140
142
|
return len(self._by_id)
|
|
141
143
|
|
|
144
|
+
def transport_readings(self) -> list[tuple[ConnId, TransportReading]]:
|
|
145
|
+
"""The latest transport reading of every connection that has one.
|
|
146
|
+
|
|
147
|
+
A connection whose transport has not taken a reading yet, or does not
|
|
148
|
+
measure its wire (it is not a :class:`TransportStatsSource`), is left
|
|
149
|
+
out.
|
|
150
|
+
"""
|
|
151
|
+
return [
|
|
152
|
+
(cid, reading)
|
|
153
|
+
for cid, conn in self._by_id.items()
|
|
154
|
+
if isinstance(conn, TransportStatsSource)
|
|
155
|
+
and (reading := conn.latest_reading) is not None
|
|
156
|
+
]
|
|
157
|
+
|
|
142
158
|
def new_conn_id(self) -> ConnId:
|
|
143
159
|
"""Mint a fresh random connection id, unique within the session.
|
|
144
160
|
|
|
@@ -51,6 +51,7 @@ from reactor_runtime.core import (
|
|
|
51
51
|
SessionState,
|
|
52
52
|
StartingInputApplied,
|
|
53
53
|
TrackDirection,
|
|
54
|
+
TrackKind,
|
|
54
55
|
Transition,
|
|
55
56
|
TransitionEvent,
|
|
56
57
|
TypeSpec,
|
|
@@ -73,10 +74,11 @@ from reactor_runtime.metrics import (
|
|
|
73
74
|
)
|
|
74
75
|
from reactor_runtime.protocol import Channel, Codec, ProtocolVersion, select
|
|
75
76
|
from reactor_runtime.recording import ClipResult, Recorder, RecorderError
|
|
76
|
-
from reactor_runtime.runner import client_stats
|
|
77
|
+
from reactor_runtime.runner import client_stats, runtime_stats
|
|
77
78
|
from reactor_runtime.runner.client_stats import ClientStatsGate
|
|
78
79
|
from reactor_runtime.runner.connection_manager import ConnectionManager
|
|
79
80
|
from reactor_runtime.runner.offer_epochs import OfferEpochs
|
|
81
|
+
from reactor_runtime.runner.runtime_stats import ModelOutput
|
|
80
82
|
from reactor_runtime.runner.session_start import (
|
|
81
83
|
InvalidSessionStartError,
|
|
82
84
|
SessionStart,
|
|
@@ -241,6 +243,11 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
241
243
|
self._sm.on_transition(self._metrics_recorder.observe)
|
|
242
244
|
self._command_metrics = CommandMetrics(self._metrics)
|
|
243
245
|
self._model_metrics = ModelMetrics(self._metrics)
|
|
246
|
+
# The same emissions, counted per session for the runtime's own stats.
|
|
247
|
+
self._model_output = ModelOutput()
|
|
248
|
+
self._output_kinds: dict[str, TrackKind] = {}
|
|
249
|
+
self._runtime_stats_interval = runtime_stats.INTERVAL_SECONDS
|
|
250
|
+
self._runtime_stats_task: asyncio.Task[None] | None = None
|
|
244
251
|
self._events = EventStream()
|
|
245
252
|
self._uploads = UploadStore()
|
|
246
253
|
self._recorder = Recorder(
|
|
@@ -377,9 +384,13 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
377
384
|
self._model_metrics.loaded(since=started_at)
|
|
378
385
|
self._bridge = bridge
|
|
379
386
|
self._command_metrics.declare(contract.commands)
|
|
380
|
-
self.
|
|
381
|
-
name
|
|
382
|
-
|
|
387
|
+
self._output_kinds = {
|
|
388
|
+
name: info.kind
|
|
389
|
+
for name, info in contract.tracks.items()
|
|
390
|
+
if info.direction is TrackDirection.OUT
|
|
391
|
+
}
|
|
392
|
+
self._model_metrics.declare(self._output_kinds.keys())
|
|
393
|
+
self._model_output.declare(self._output_kinds.keys())
|
|
383
394
|
self._sm.send(SessionEvent.INITIALIZATION_SUCCESS)
|
|
384
395
|
logger.info(
|
|
385
396
|
"model loaded; session ready",
|
|
@@ -414,6 +425,11 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
414
425
|
thing released even when ``stop`` races a session that is still closing.
|
|
415
426
|
"""
|
|
416
427
|
self._cancel_orphan_timeout()
|
|
428
|
+
task = self._cancel_runtime_stats()
|
|
429
|
+
if task is not None:
|
|
430
|
+
# Waits for the reporter to finish without raising its
|
|
431
|
+
# cancellation, which it never caught if it had not started yet.
|
|
432
|
+
await asyncio.wait([task])
|
|
417
433
|
await self._drain_teardown()
|
|
418
434
|
await asyncio.to_thread(self._recorder.close)
|
|
419
435
|
if self._step_store is not None:
|
|
@@ -1295,6 +1311,10 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
1295
1311
|
"""
|
|
1296
1312
|
for track in chunk.bundle.tracks:
|
|
1297
1313
|
self._model_metrics.emitted(track, chunk.n_frames)
|
|
1314
|
+
# A chunk the model emitted for an earlier session (from its
|
|
1315
|
+
# session-end hook, say, after the next session started) still
|
|
1316
|
+
# plays out, but it is not this session's output.
|
|
1317
|
+
self._model_output.emitted(track, chunk.n_frames, chunk.session)
|
|
1298
1318
|
generation = self._media_generation
|
|
1299
1319
|
self._connections.broadcast_media(chunk, abort=lambda: self._media_generation != generation)
|
|
1300
1320
|
# The archive takes the whole chunk even when a flush cut the broadcast
|
|
@@ -1522,6 +1542,7 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
1522
1542
|
entered = transition.from_state is not transition.to_state
|
|
1523
1543
|
if entered:
|
|
1524
1544
|
self._reset_orphan_timeout(transition.to_state)
|
|
1545
|
+
self._sync_runtime_stats(transition.to_state)
|
|
1525
1546
|
if entered and transition.to_state is SessionState.CLOSING and self._loop is not None:
|
|
1526
1547
|
reason = transition.detail.get("reason", EndReason.STOPPED)
|
|
1527
1548
|
close_reason = transition.detail.get("close_reason", "")
|
|
@@ -1654,6 +1675,9 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
1654
1675
|
"""
|
|
1655
1676
|
if transition.is_session_start:
|
|
1656
1677
|
self._sessions_posted += 1
|
|
1678
|
+
# Before the model hears of the session: from then on its thread
|
|
1679
|
+
# can emit, and every frame it emits for this session counts.
|
|
1680
|
+
self._model_output.reset(self._sessions_posted)
|
|
1657
1681
|
bridge.dispatch_reactor_event(
|
|
1658
1682
|
SessionStarted(
|
|
1659
1683
|
self._session_id,
|
|
@@ -1714,6 +1738,63 @@ class Runner(ServiceComponent, ConnectionSink):
|
|
|
1714
1738
|
return
|
|
1715
1739
|
self._sm.send(SessionEvent.TIMEOUT, reason=EndReason.TIMED_OUT)
|
|
1716
1740
|
|
|
1741
|
+
# -- runtime stats --------------------------------------------------------
|
|
1742
|
+
|
|
1743
|
+
def _sync_runtime_stats(self, state: SessionState) -> None:
|
|
1744
|
+
"""Run the runtime stats reporter while a session runs, and only then."""
|
|
1745
|
+
if state in _RUNNING_STATES:
|
|
1746
|
+
if self._runtime_stats_task is None and self._loop is not None:
|
|
1747
|
+
self._runtime_stats_task = self._loop.create_task(self._report_runtime_stats())
|
|
1748
|
+
else:
|
|
1749
|
+
self._cancel_runtime_stats()
|
|
1750
|
+
|
|
1751
|
+
def _cancel_runtime_stats(self) -> asyncio.Task[None] | None:
|
|
1752
|
+
"""Cancel the runtime stats reporter, returning its task to await, if one ran."""
|
|
1753
|
+
task, self._runtime_stats_task = self._runtime_stats_task, None
|
|
1754
|
+
if task is not None:
|
|
1755
|
+
task.cancel()
|
|
1756
|
+
return task
|
|
1757
|
+
|
|
1758
|
+
async def _report_runtime_stats(self) -> None:
|
|
1759
|
+
"""Journal a ``runtime_stats`` reading on a fixed cadence until cancelled.
|
|
1760
|
+
|
|
1761
|
+
A reading that fails to build is logged and skipped rather than ending
|
|
1762
|
+
the reporter: stats are best-effort, so one bad sample must not stop
|
|
1763
|
+
them for the rest of the session.
|
|
1764
|
+
"""
|
|
1765
|
+
try:
|
|
1766
|
+
while True:
|
|
1767
|
+
await asyncio.sleep(self._runtime_stats_interval)
|
|
1768
|
+
try:
|
|
1769
|
+
self._journal_runtime_stats()
|
|
1770
|
+
except Exception:
|
|
1771
|
+
logger.warning("runtime stats reading failed", exc_info=True)
|
|
1772
|
+
except asyncio.CancelledError:
|
|
1773
|
+
return
|
|
1774
|
+
|
|
1775
|
+
def _journal_runtime_stats(self) -> None:
|
|
1776
|
+
"""Journal the runtime's own view of the session as one ``metric`` fact.
|
|
1777
|
+
|
|
1778
|
+
The fact is a self-loop named ``runtime_stats``, the runtime's
|
|
1779
|
+
counterpart to a client's ``client_stats``: what the model emitted on
|
|
1780
|
+
each output track over the last window, and each connection's latest
|
|
1781
|
+
transport reading, in the metrics its transport names. Shaped by
|
|
1782
|
+
:func:`runtime_stats.to_detail`.
|
|
1783
|
+
"""
|
|
1784
|
+
if self._sm.current_state not in _RUNNING_STATES:
|
|
1785
|
+
return
|
|
1786
|
+
self._sm.send(
|
|
1787
|
+
SessionEvent.METRIC,
|
|
1788
|
+
name="runtime_stats",
|
|
1789
|
+
session_id=self._recording_id,
|
|
1790
|
+
**runtime_stats.to_detail(
|
|
1791
|
+
observed_at_ms=int(time.time() * 1000),
|
|
1792
|
+
model_output=self._model_output.take(),
|
|
1793
|
+
output_kinds=self._output_kinds,
|
|
1794
|
+
readings=self._connections.transport_readings(),
|
|
1795
|
+
),
|
|
1796
|
+
)
|
|
1797
|
+
|
|
1717
1798
|
async def _close_session(self, reason: EndReason) -> None:
|
|
1718
1799
|
"""Close the session's connections, then mark cleanup complete.
|
|
1719
1800
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Count what the model emits during a session, for the runtime's own stats.
|
|
2
|
+
|
|
3
|
+
The runtime reports its own view of a session alongside what clients report
|
|
4
|
+
about theirs. The model's output is the part of that view no transport sees:
|
|
5
|
+
how many frames it produced on each output track and how evenly it produced
|
|
6
|
+
them. :class:`ModelOutput` keeps those counts for the session that is running.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from reactor_runtime.core import (
|
|
18
|
+
ClientTrackDirection,
|
|
19
|
+
ConnId,
|
|
20
|
+
TrackKind,
|
|
21
|
+
TransportReading,
|
|
22
|
+
TransportTrackReading,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModelOutput:
|
|
27
|
+
"""Per-session counts of the frames the model emitted, by output track.
|
|
28
|
+
|
|
29
|
+
Emissions are counted from the thread the model emits on, and a reading is
|
|
30
|
+
taken from the runtime's loop, so every access holds a lock. A reading
|
|
31
|
+
covers the window since the previous one: the frame rate and the longest
|
|
32
|
+
gap are what that window saw, while the frame total runs for the whole
|
|
33
|
+
session. :meth:`reset` starts a new session from zero, and an emission
|
|
34
|
+
stamped with another session's number is left out of its count.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, clock: Callable[[], float] = time.monotonic) -> None:
|
|
38
|
+
self._clock = clock
|
|
39
|
+
self._lock = threading.Lock()
|
|
40
|
+
self._tracks: tuple[str, ...] = ()
|
|
41
|
+
self._frames: dict[str, int] = {}
|
|
42
|
+
self._window_frames: dict[str, int] = {}
|
|
43
|
+
self._last_emit: dict[str, float] = {}
|
|
44
|
+
self._longest_gap: dict[str, float] = {}
|
|
45
|
+
self._window_start = clock()
|
|
46
|
+
self._session: int | None = None
|
|
47
|
+
|
|
48
|
+
def declare(self, tracks: Iterable[str]) -> None:
|
|
49
|
+
"""Name the output tracks the model declares.
|
|
50
|
+
|
|
51
|
+
A declared track the model has emitted nothing on reads zero frames
|
|
52
|
+
rather than being absent, which tells a silent track apart from a
|
|
53
|
+
track this model does not have.
|
|
54
|
+
"""
|
|
55
|
+
with self._lock:
|
|
56
|
+
self._tracks = tuple(tracks)
|
|
57
|
+
|
|
58
|
+
def reset(self, session: int | None = None) -> None:
|
|
59
|
+
"""Start counting *session* from zero.
|
|
60
|
+
|
|
61
|
+
The gap between the last frame of one session and the first of the
|
|
62
|
+
next is the model waiting for a client, so no gap crosses the reset.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
session: The number of the session that starts, as the model counts
|
|
66
|
+
them. From here on, an emission stamped with any other number
|
|
67
|
+
belongs to another session and is not counted.
|
|
68
|
+
"""
|
|
69
|
+
with self._lock:
|
|
70
|
+
self._session = session
|
|
71
|
+
self._frames.clear()
|
|
72
|
+
self._window_frames.clear()
|
|
73
|
+
self._last_emit.clear()
|
|
74
|
+
self._longest_gap.clear()
|
|
75
|
+
self._window_start = self._clock()
|
|
76
|
+
|
|
77
|
+
def emitted(self, track: str, frames: int, session: int | None = None) -> None:
|
|
78
|
+
"""Count one emission of *frames* frames on *track*.
|
|
79
|
+
|
|
80
|
+
Counted in frames because one emission can carry a batch of them. The
|
|
81
|
+
gap is measured between emissions, undivided by the batch, so a model
|
|
82
|
+
that emits a batch at a time has the play-out length of a batch as its
|
|
83
|
+
normal gap and a stall shows above it.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
track: The output track the frames were emitted on.
|
|
87
|
+
frames: How many frames the emission carried.
|
|
88
|
+
session: The number of the session the model emitted them in, or
|
|
89
|
+
``None`` when the emitter does not count sessions. An emission
|
|
90
|
+
of a session other than the one :meth:`reset` started is not
|
|
91
|
+
counted. The check runs under the same lock as the reset, so
|
|
92
|
+
an emission is counted for the session that is current when
|
|
93
|
+
it lands.
|
|
94
|
+
"""
|
|
95
|
+
with self._lock:
|
|
96
|
+
if session is not None and self._session is not None and session != self._session:
|
|
97
|
+
return
|
|
98
|
+
# Read under the lock, so concurrent emissions store their times
|
|
99
|
+
# in the order they were taken.
|
|
100
|
+
now = self._clock()
|
|
101
|
+
previous = self._last_emit.get(track)
|
|
102
|
+
if previous is not None:
|
|
103
|
+
gap = now - previous
|
|
104
|
+
if gap > self._longest_gap.get(track, 0.0):
|
|
105
|
+
self._longest_gap[track] = gap
|
|
106
|
+
self._last_emit[track] = now
|
|
107
|
+
self._frames[track] = self._frames.get(track, 0) + frames
|
|
108
|
+
self._window_frames[track] = self._window_frames.get(track, 0) + frames
|
|
109
|
+
|
|
110
|
+
def take(self) -> dict[str, dict[str, float]]:
|
|
111
|
+
"""Read every output track's counts and start the next window.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Per track name, its metrics: ``frames_emitted`` (the session's
|
|
115
|
+
total), ``frames_per_second`` (over the window), and, once the track
|
|
116
|
+
has emitted, ``max_emit_interval_ms`` (the longest gap the window
|
|
117
|
+
saw, when it saw two emissions) and ``ms_since_last_emit``, which
|
|
118
|
+
shows a stall that is still going before the next emission ends it.
|
|
119
|
+
"""
|
|
120
|
+
with self._lock:
|
|
121
|
+
now = self._clock()
|
|
122
|
+
elapsed = now - self._window_start
|
|
123
|
+
names = dict.fromkeys((*self._tracks, *self._frames))
|
|
124
|
+
readings: dict[str, dict[str, float]] = {}
|
|
125
|
+
for name in names:
|
|
126
|
+
metrics: dict[str, float] = {
|
|
127
|
+
"frames_emitted": float(self._frames.get(name, 0)),
|
|
128
|
+
}
|
|
129
|
+
if elapsed > 0:
|
|
130
|
+
metrics["frames_per_second"] = self._window_frames.get(name, 0) / elapsed
|
|
131
|
+
if name in self._longest_gap:
|
|
132
|
+
metrics["max_emit_interval_ms"] = self._longest_gap[name] * 1000.0
|
|
133
|
+
last = self._last_emit.get(name)
|
|
134
|
+
if last is not None:
|
|
135
|
+
metrics["ms_since_last_emit"] = (now - last) * 1000.0
|
|
136
|
+
readings[name] = metrics
|
|
137
|
+
self._window_frames.clear()
|
|
138
|
+
self._longest_gap.clear()
|
|
139
|
+
self._window_start = now
|
|
140
|
+
return readings
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
INTERVAL_SECONDS = 5.0
|
|
144
|
+
"""How often the runner journals a ``runtime_stats`` reading during a session."""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def to_detail(
|
|
148
|
+
*,
|
|
149
|
+
observed_at_ms: int,
|
|
150
|
+
model_output: Mapping[str, Mapping[str, float]],
|
|
151
|
+
output_kinds: Mapping[str, TrackKind],
|
|
152
|
+
readings: Iterable[tuple[ConnId, TransportReading]],
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""Shape one ``runtime_stats`` reading as a ``metric`` fact's detail.
|
|
155
|
+
|
|
156
|
+
The shape follows a ``client_stats`` reading's, so a consumer reads both
|
|
157
|
+
the same way: tracks carry ``track_name``, ``kind``, ``direction`` (as the
|
|
158
|
+
client sees the track) and ``codec``, beside their ``metrics``. A metric
|
|
159
|
+
value that isn't a finite number is left out, since JSON has no way to
|
|
160
|
+
carry it.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
observed_at_ms: When the reading was taken, in Unix milliseconds.
|
|
164
|
+
model_output: Per output track, the metrics :meth:`ModelOutput.take`
|
|
165
|
+
returned.
|
|
166
|
+
output_kinds: The kind of each output track the model declares.
|
|
167
|
+
readings: Each connection's latest transport reading.
|
|
168
|
+
"""
|
|
169
|
+
return {
|
|
170
|
+
"observed_at": observed_at_ms,
|
|
171
|
+
"model_output": [
|
|
172
|
+
{
|
|
173
|
+
"track_name": name,
|
|
174
|
+
"kind": str(output_kinds[name]) if name in output_kinds else "",
|
|
175
|
+
# What the model sends, the client receives.
|
|
176
|
+
"direction": str(ClientTrackDirection.RECVONLY),
|
|
177
|
+
"metrics": _finite(metrics),
|
|
178
|
+
}
|
|
179
|
+
for name, metrics in model_output.items()
|
|
180
|
+
],
|
|
181
|
+
"connection_stats": [
|
|
182
|
+
{
|
|
183
|
+
"conn_id": int(cid),
|
|
184
|
+
"metrics": _finite(reading.metrics),
|
|
185
|
+
"track_stats": [_track(track) for track in reading.tracks],
|
|
186
|
+
}
|
|
187
|
+
for cid, reading in readings
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _track(track: TransportTrackReading) -> dict[str, Any]:
|
|
193
|
+
return {
|
|
194
|
+
"track_name": track.track_name,
|
|
195
|
+
"kind": str(track.kind) if track.kind is not None else "",
|
|
196
|
+
"direction": str(track.direction),
|
|
197
|
+
"codec": track.codec,
|
|
198
|
+
"metrics": _finite(track.metrics),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _finite(metrics: Mapping[str, float]) -> dict[str, float]:
|
|
203
|
+
"""Keep the values JSON can carry: finite numbers."""
|
|
204
|
+
return {name: float(value) for name, value in metrics.items() if math.isfinite(value)}
|
{reactor_runtime-3.7.0 → reactor_runtime-3.8.0}/src/reactor_runtime/transport/webrtc/connection.py
RENAMED
|
@@ -25,12 +25,14 @@ from reactor_runtime.core import (
|
|
|
25
25
|
TrackDirection,
|
|
26
26
|
TrackInfo,
|
|
27
27
|
TrackKind,
|
|
28
|
+
TransportReading,
|
|
28
29
|
)
|
|
29
30
|
from reactor_runtime.protocol import Channel, ProtocolVersion
|
|
30
31
|
from reactor_runtime.transport.webrtc.client_stats import read_client_stats
|
|
31
32
|
from reactor_runtime.transport.webrtc.config import WebRtcConfig
|
|
32
33
|
from reactor_runtime.transport.webrtc.pacer import MediaPacer
|
|
33
34
|
from reactor_runtime.transport.webrtc.peer import WebRTCPeer, WebRtcPeerFactory
|
|
35
|
+
from reactor_runtime.transport.webrtc.reading import transport_reading
|
|
34
36
|
from reactor_runtime.transport.webrtc.signaling import IceCandidate, SdpAnswer, SdpOffer, TrackMap
|
|
35
37
|
from reactor_runtime.transport.webrtc.stats import OutboundMediaHealth, PeerStats
|
|
36
38
|
|
|
@@ -105,6 +107,8 @@ class WebRTCConnection:
|
|
|
105
107
|
self._watchdog_task: asyncio.Task[None] | None = None
|
|
106
108
|
self._stats_task: asyncio.Task[None] | None = None
|
|
107
109
|
self._latest_stats: PeerStats | None = None
|
|
110
|
+
self._latest_reading: TransportReading | None = None
|
|
111
|
+
self._sampled_at: float | None = None
|
|
108
112
|
# The counters as of the previous sample, so each report is what the
|
|
109
113
|
# window cost rather than what the connection has cost since it opened.
|
|
110
114
|
self._reported_media = OutboundMediaHealth()
|
|
@@ -152,6 +156,15 @@ class WebRTCConnection:
|
|
|
152
156
|
"""The most recent stats sample, or ``None`` before the first cycle."""
|
|
153
157
|
return self._latest_stats
|
|
154
158
|
|
|
159
|
+
@property
|
|
160
|
+
def latest_reading(self) -> TransportReading | None:
|
|
161
|
+
"""The most recent stats sample as a neutral reading, or ``None`` before the first.
|
|
162
|
+
|
|
163
|
+
Each track's bitrate is measured between this sample and the one before
|
|
164
|
+
it, so the reading of a connection's first sample has none.
|
|
165
|
+
"""
|
|
166
|
+
return self._latest_reading
|
|
167
|
+
|
|
155
168
|
@property
|
|
156
169
|
def protocol_version(self) -> ProtocolVersion:
|
|
157
170
|
"""The wire codec the connection negotiated, held by its peer."""
|
|
@@ -365,7 +378,11 @@ class WebRTCConnection:
|
|
|
365
378
|
media=replace(stats.media, dropped_frames=self._pacer.dropped_frames),
|
|
366
379
|
)
|
|
367
380
|
self._report_media_health(stats.media)
|
|
381
|
+
now = time.monotonic()
|
|
382
|
+
elapsed = now - self._sampled_at if self._sampled_at is not None else None
|
|
383
|
+
self._latest_reading = transport_reading(stats, self._latest_stats, elapsed)
|
|
368
384
|
self._latest_stats = stats
|
|
385
|
+
self._sampled_at = now
|
|
369
386
|
if self._on_stats is not None:
|
|
370
387
|
self._on_stats(stats)
|
|
371
388
|
except asyncio.CancelledError:
|