This document explains how video-related performance is instrumented in the Yral Android application (Kotlin). It is intended for developers who need to:
- understand the existing infrastructure,
- debug/verify traces in Firebase Performance, or
- add new performance measurements.
| Term | Description |
|---|---|
| Trace | A named timer created in Firebase Performance that starts and stops around an app flow. Exposed through FirebaseOperationTrace. |
| Metric | Numeric measurement attached to a trace – e.g. buffering_count. |
| Attribute | Key-value metadata attached to a trace – e.g. video_id, video_format. |
The PerformanceConstants object defines standard keys and values used across all traces:
// Attribute keys
const val RESULT_KEY = "result"
const val URL_KEY = "url"
const val MODULE_KEY = "module"
const val OPERATION_KEY = "operation"
// Result values
const val SUCCESS_VALUE = "success"
const val ERROR_VALUE = "error"
// Common metrics
const val DURATION_MS = "duration_ms"
const val COUNT_METRIC = "count"| Trace name (Firebase) | Kotlin enum / constant | When it starts | When it ends |
|---|---|---|---|
VideoStartup |
TraceType.LOAD_TRACE |
Player instance is setup (network request on HLS / progressive starts) | First onBuffer() OR any error / manual cleanup |
FirstFrame |
TraceType.FIRST_FRAME_TRACE |
Right after player setup | When the first decoded frame is ready (onReady()) |
VideoPlayback |
TraceType.PLAYBACK_TRACE |
Immediately before playback starts (onPlayBackStarted()) |
When the video ends (onEnd()) OR is stopped / errored |
VideoStartup_prefetch |
PrefetchTraceType.LOAD_TRACE |
When a video is prefetched in background | First buffer OR error / idle |
VideoReady_prefetch |
PrefetchTraceType.READY_TRACE |
Prefetch request initiated | When the pre-downloaded asset is ready for render |
All trace names live in VideoPerformanceConstants.
When stopping a trace, it should be marked with one of these states:
- Success: Call
stopTraceWithSuccess()when the operation completes normally - Error: Call
stopTraceWithError()when an error occurs - Neutral: Call
stopTrace()for normal cleanup without marking success/failure
The performance monitoring is event-driven, based on callbacks from the video player.
flowchart TD
VP[Video Player] --> |events| VL[/"VideoListener\ninterface"/]
VL --> |implements| VLI[VideoListenerImpl]
VLI --> |uses| PM[/"PerformanceMonitor\ninterface"/]
subgraph Trace Management
direction TB
TC{Trace Check} --> |if not traced| ST[Start Trace]
ST --> FOT[FirebaseOperationTrace]
FOT --> |success| RT[Register Trace]
FOT --> |error| ET[Error State]
FOT --> |cleanup| CT[Clean Trace]
end
VLI --> |isTraced check| TC
subgraph Factory
direction LR
VPF[VideoPerformanceFactoryProvider]
VPF --> |creates| LT[LoadTimeTrace]
VPF --> |creates| FFT[FirstFrameTrace]
VPF --> |creates| PT[PlaybackTimeTrace]
VPF --> |creates| PRT[PrefetchReadyTrace]
VPF --> |creates| PLT[PrefetchLoadTimeTrace]
end
ST --> |via factory| VPF
subgraph Attributes
direction TB
ATT[Trace Attributes]
ATT --> |video_id| ID[Reel ID]
ATT --> |video_format| FMT[HLS/Progressive]
ATT --> |module| MOD[video_player]
ATT --> |result| RES[success/error]
end
FOT --> |sets| ATT
FB[(Firebase\nPerformance)]
FOT --> |reports| FB
classDef interface fill:#f9f,stroke:#333,stroke-width:2px
classDef implementation fill:#fff,stroke:#333
classDef external fill:#bbf,stroke:#333
class VL,PM interface
class VP,FB external
class VLI,FOT,VPF implementation
VideoListenerImplsubscribes to player callbacks defined byVideoListener.- For each callback (e.g.,
onBuffer,onReady), it delegates to thePerformanceMonitorAPI:startTrace(TraceType)stopTrace,stopTraceWithSuccess,stopTraceWithError
- Traces are concrete subclasses of
FirebaseOperationTraceproduced byVideoPerformanceFactoryProvider. - Attributes added automatically in the trace constructor:
video_id– unique id of the reelvideo_format–hlsorprogressivemodule– set to "video_player" for all video traces
Example: Starting Traces
When the player is set up, both LOAD_TRACE and FIRST_FRAME_TRACE are initiated.
override fun onSetupPlayer() {
startTrace(TraceType.LOAD_TRACE)
startTrace(TraceType.FIRST_FRAME_TRACE)
}Example: Incrementing a Metric During playback, the implementation increments custom metrics:
incrementMetric(
type = TraceType.PLAYBACK_TRACE,
metricName = VideoPerformanceConstants.VideoPerformanceMetric.BUFFERING_COUNT.key,
)This appears in Firebase as the buffering_count metric on the VideoPlayback trace.
PrefetchVideoListenerImpl mirrors the same life-cycle, but operates on PrefetchTraceType to instrument background downloads (pre-buffering upcoming videos).
To avoid recording the same trace (e.g. VideoStartup) multiple times for the same video, VideoListenerImpl uses a check before starting a trace:
if (!enablePerformanceTracing || isTraced(reel.videoId, type)) returnThe isTraced function (passed in the constructor) checks a session-level cache. Once a trace for a specific video ID completes successfully, registerTrace is called, which flags that combination as complete, preventing redundant tracing.
- Define the trace name in
VideoPerformanceConstants. - Create a subclass of
FirebaseOperationTraceand add a factory method insideVideoPerformanceFactoryProvider. - Emit the new
start/stopcalls from the appropriate listeners (VideoListenerImpl,PrefetchVideoListenerImpl, etc.). - Set module attribution using
setModule("video_player")in the trace constructor. - (Optional) Extend
VideoPerformanceMetricenum for any custom metric names.
- Build and run the debug flavour with
enablePerformanceTracing = true. - Interact with Feeds – watch a couple of videos.
- Open Firebase → Performance → Traces.
- Filter by the respective trace name, e.g.
VideoStartup. - You should see:
- Attributes:
video_id,video_format,module - Result state:
successorerror - Any custom metrics you incremented
- Attributes:
If traces are missing ensure:
- You start and stop every trace.
- No trace is left running after the screen is destroyed. The
onIdle()callback in the listener is the main cleanup hook and should stop all active traces.
Both VideoListenerImpl and PrefetchVideoListenerImpl guard calls behind the enablePerformanceTracing boolean. Flip it to false when profiling locally without network noise.
| Kotlin enum | Firebase key | Description |
|---|---|---|
BUFFERING_COUNT |
buffering_count |
Number of times ExoPlayer fired buffering during playback |
Feel free to extend the enum as your feature requires.
This project uses three build systems across its stack. The table below maps each concept to its equivalent in Rust (a familiar reference point) and in the iOS layer.
| Concept | Rust | Gradle/KMM | Swift/iOS (this repo) |
|---|---|---|---|
| Workspace membership | [workspace] members in root Cargo.toml |
settings.gradle.kts | iosApp.xcodeproj — targets listed in Xcode project |
| Centralized dependency versions | [workspace.dependencies] |
gradle/libs.versions.toml | FIREBASE_VERSION = '...' variable in iosApp/Podfile — manual, no real equivalent |
| Per-module build config | Each crate's Cargo.toml |
Each module's build.gradle.kts |
Each Xcode target (Yral, Yral-Staging, YralTests) |
| Shared/global build config | Root Cargo.toml [profile.*] |
Root build.gradle.kts — applies ktlint, detekt, shared repos to all subprojects | Podfile def add_common_pods block |
| Lock file | Cargo.lock |
Gradle build cache / lock files | Podfile.lock |
| Package registry | crates.io | Maven Central + Google Maven | CocoaPods CDN (source line in Podfile) |
| Build command | cargo build |
./gradlew build |
xcodebuild / Xcode IDE |
libs.versions.toml is not the workspace declaration — that's settings.gradle.kts. It is specifically the centralized version-pinning layer, equivalent to [workspace.dependencies] in a Rust workspace.
A child module consuming a version looks like:
// any module's build.gradle.kts
implementation(libs.koin.core) // version resolved from libs.versions.toml# any crate's Cargo.toml
koin = { workspace = true } # version resolved from [workspace.dependencies]Unlike Gradle's libs.versions.toml, CocoaPods has no workspace-wide version catalog. The FIREBASE_VERSION Ruby variable in the Podfile is a local convention — versions for KMM-side Firebase dependencies are pinned separately in libs.versions.toml, so they must be kept in sync manually.