> ## Documentation Index
> Fetch the complete documentation index at: https://www.helius.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Mengukur Latensi

> Pelajari cara mengukur dan menganalisis latensi untuk aliran gRPC dengan benar menggunakan berbagai metode pengujian.

<Warning>
  **PENAFIAN PENTING: Hanya untuk Pusat Data Produksi**

  Pengujian latensi ini dirancang **hanya untuk lingkungan pusat data produksi**. JANGAN jalankan pengujian ini di mesin lokal atau koneksi internet konsumen. Bandwidth lokal tidak mampu menangani langganan Solana yang berat dan akan menghasilkan data yang tidak bermakna serta tidak mencerminkan performa di dunia nyata.
</Warning>

<Warning>
  **PERSYARATAN KOLOKASI: Terapkan di Dekat Endpoint LaserStream Anda**

  Untuk mendapatkan pengukuran latensi yang bermakna, Anda **harus** menempatkan infrastruktur pengujian di wilayah yang sama dengan endpoint LaserStream pilihan Anda. Jarak jaringan akan mendominasi hasil pengukuran Anda—pengujian dari benua lain akan menunjukkan latensi jaringan, bukan performa LaserStream.
</Warning>

***

## Memahami latensi dalam sistem blockchain terdistribusi

Saat bekerja dengan layanan streaming blockchain, pengukuran latensi menjadi rumit karena sistem terdistribusi tidak memiliki jam universal. Tidak seperti sistem tradisional yang memungkinkan Anda mengukur waktu perjalanan bolak-balik ke satu server, jaringan blockchain melibatkan banyak validator yang masing-masing menerima dan memproses transaksi yang sama pada waktu berbeda.

**Tantangan mendasar:** Blockchain seperti Solana tidak memiliki konsep waktu absolut. Setiap node validator di seluruh dunia akan menerima transaksi yang sama pada waktu berbeda, dan konfirmasi bergantung pada persentase klaster yang mencapai konsensus. Hal ini membuat pengukuran latensi deterministik mustahil dilakukan dalam pengertian tradisional.

## Tingkat komitmen dan prioritas latensi

Solana menawarkan tiga tingkat komitmen, masing-masing dengan karakteristik latensi yang berbeda:

* **Processed**: Tercepat, konfirmasi oleh satu validator (\~400 md)
* **Confirmed**: Sedang, konfirmasi oleh mayoritas super (\~2–3 detik)
* **Finalized**: Paling lambat, finalisasi jaringan secara menyeluruh (\~15–30 detik)

Untuk aplikasi yang sensitif terhadap latensi, **komitmen processed** biasanya menjadi target. Semua pengujian dalam panduan ini menggunakan tingkat komitmen processed karena sebagian besar kasus penggunaan berfrekuensi tinggi memprioritaskan kecepatan dibandingkan finalitas absolut.

## Tiga pendekatan untuk mengukur latensi

### 1. Membandingkan aliran gRPC paralel

**Metode paling andal**—Membandingkan dua aliran independen ke sumber data yang sama untuk mengukur aliran mana yang menerima peristiwa identik terlebih dahulu.

**Keunggulan:**

* Menghilangkan masalah sinkronisasi jam
* Menyediakan perbandingan performa relatif
* Paling akurat untuk membandingkan layanan

### 2. Perbandingan stempel waktu lokal dengan created\_at

**Keandalan sedang**—Mengukur selisih antara waktu sistem Anda menerima pesan dan stempel waktu yang disematkan dalam pesan oleh layanan LaserStream.

**Keterbatasan:**

* Hanya merepresentasikan waktu saat LaserStream membuat pesan secara internal
* Penundaan upstream menuju LaserStream tidak akan tercatat
* Kurang akurat dibandingkan Metode 1 untuk latensi ujung ke ujung yang sebenarnya

### 3. Analisis stempel waktu blok (tidak direkomendasikan)

**Tidak direkomendasikan**—Membandingkan waktu penerimaan lokal dengan stempel waktu blok Solana.

**Keterbatasan signifikan:**

* Stempel waktu blok hanya memiliki tingkat ketelitian dalam detik
* Solana menghasilkan blok setiap 400 md
* Hanya menyediakan sedikit informasi yang berguna

***

## Persyaratan penyiapan

### Kolokasi regional

Untuk mendapatkan pengukuran latensi yang bermakna, terapkan infrastruktur pengujian Anda di pusat data atau wilayah yang sama dengan endpoint LaserStream Anda.

**Wilayah LaserStream yang tersedia:**

* **ewr**: New York, AS (Pantai Timur) - `https://laserstream-mainnet-ewr.helius-rpc.com`
* **pitt**: Pittsburgh, AS (Tengah) - `https://laserstream-mainnet-pitt.helius-rpc.com`
* **slc**: Salt Lake City, AS (Pantai Barat) - `https://laserstream-mainnet-slc.helius-rpc.com`
* **ams**: Amsterdam, Eropa - `https://laserstream-mainnet-ams.helius-rpc.com`
* **fra**: Frankfurt, Eropa - `https://laserstream-mainnet-fra.helius-rpc.com`
* **tyo**: Tokyo, Asia - `https://laserstream-mainnet-tyo.helius-rpc.com`
* **sgp**: Singapura, Asia - `https://laserstream-mainnet-sgp.helius-rpc.com`

Untuk pengujian devnet, gunakan: `https://laserstream-devnet-ewr.helius-rpc.com`

Lihat [dokumentasi gRPC LaserStream](/docs/id/laserstream/grpc) untuk petunjuk penyiapan lengkap dan panduan pemilihan endpoint.

### Penyiapan lingkungan Rust

Semua skrip pengukuran menggunakan Rust dengan Cargo. Penyiapan dasar:

```bash theme={"system"}
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Setup project
cargo new latency-testing
cd latency-testing

# Add dependencies to Cargo.toml
[dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }
# Futures utilities for StreamExt, SinkExt
futures = "0.3"
# Environment variable loading
dotenvy = "0.15"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
# Yellowstone gRPC client and protocol
yellowstone-grpc-client = "13.3.0"
yellowstone-grpc-proto = "12.6.0"
# For timestamp analysis
prost-types = "0.14"
```

Buat file `.env` dengan kredensial Anda:

```bash theme={"system"}
YS_GRPC_URL=your-comparison-endpoint
YS_API_KEY=your-comparison-api-key
LS_GRPC_URL=your-laserstream-endpoint  
LS_API_KEY=your-helius-api-key
```

Dapatkan kunci API Helius Anda dari [Dasbor Helius](https://dashboard.helius.dev/). Devnet LaserStream tersedia di semua paket. Akses mainnet memerlukan paket Business atau Professional.

***

## Metode 1: Perbandingan aliran paralel

Skrip ini membuat dua koneksi independen ke endpoint gRPC yang berbeda dan mengukur koneksi mana yang menerima pesan `BlockMeta` yang sama terlebih dahulu. Pendekatan ini menghilangkan masalah sinkronisasi jam dengan menggunakan pengukuran waktu relatif.

```rust [expandable] theme={"system"}
use std::collections::HashMap;
use std::time::SystemTime;

use dotenvy::dotenv;
use futures::{StreamExt, SinkExt};
use tokio::sync::mpsc;
use tracing::{debug, error, info};

use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient};
use yellowstone_grpc_proto::prelude::{
    subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest, 
    SubscribeRequestFilterBlocksMeta, SubscribeUpdate,
};

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum Source {
    Yellowstone,
    Laserstream,
}

#[derive(Default, Debug)]
struct SlotTimings {
    ys_recv_ms: Option<i128>,
    ls_recv_ms: Option<i128>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = dotenv();
    tracing_subscriber::fmt().with_env_filter("info").init();

    let ys_url = std::env::var("YS_GRPC_URL").expect("YS_GRPC_URL env variable not set");
    let ls_url = std::env::var("LS_GRPC_URL").expect("LS_GRPC_URL env variable not set");
    let ys_api_key = std::env::var("YS_API_KEY").ok();
    let ls_api_key = std::env::var("LS_API_KEY").ok();

    let commitment = CommitmentLevel::Processed;
    info!(?commitment, "Starting latency comparison");

    // Establish both clients
    let mut ys_client = GeyserGrpcClient::build_from_shared(ys_url.clone())
        .expect("invalid YS url")
        .x_token(ys_api_key.clone())?
        .tls_config(ClientTlsConfig::new().with_native_roots())?
        .max_decoding_message_size(10 * 1024 * 1024)
        .connect()
        .await?;

    let mut ls_client = GeyserGrpcClient::build_from_shared(ls_url.clone())
        .expect("invalid LS url")
        .x_token(ls_api_key.clone())?
        .tls_config(ClientTlsConfig::new().with_native_roots())?
        .max_decoding_message_size(10 * 1024 * 1024)
        .connect()
        .await?;

    let (mut ys_tx, mut ys_rx) = ys_client.subscribe().await?;
    let (mut ls_tx, mut ls_rx) = ls_client.subscribe().await?;
    
    let subscribe_request = SubscribeRequest {
        blocks_meta: {
            let mut m = HashMap::<String, SubscribeRequestFilterBlocksMeta>::new();
            m.insert("all".to_string(), SubscribeRequestFilterBlocksMeta::default());
            m
        },
        commitment: Some(commitment as i32),
        ..Default::default()
    };

    ys_tx.send(subscribe_request.clone()).await?;
    ls_tx.send(subscribe_request).await?;

    let (agg_tx, mut agg_rx) = mpsc::unbounded_channel::<(Source, u64, i128)>();

    // Spawn task for Yellowstone stream
    {
        let agg_tx = agg_tx.clone();
        tokio::spawn(async move {
            while let Some(update_res) = ys_rx.next().await {
                match update_res {
                    Ok(update) => handle_update(Source::Yellowstone, update, &agg_tx).await,
                    Err(e) => {
                        error!(target: "ys", "stream error: {:?}", e);
                        break;
                    }
                }
            }
        });
    }

    // Spawn task for Laserstream stream
    {
        let agg_tx = agg_tx.clone();
        tokio::spawn(async move {
            while let Some(update_res) = ls_rx.next().await {
                match update_res {
                    Ok(update) => handle_update(Source::Laserstream, update, &agg_tx).await,
                    Err(e) => {
                        error!(target: "ls", "stream error: {:?}", e);
                        break;
                    }
                }
            }
        });
    }

    // Aggregator – collect latencies per slot and print once we have both sources
    let mut timings: HashMap<u64, SlotTimings> = HashMap::new();
    let mut deltas: Vec<i128> = Vec::new();
    let mut count = 0;

    println!("slot,ys_recv_ms,ls_recv_ms,delta_ms");

    while let Some((source, slot, latency_ms)) = agg_rx.recv().await {
        let entry = timings.entry(slot).or_default();
        match source {
            Source::Yellowstone => entry.ys_recv_ms = Some(latency_ms),
            Source::Laserstream => entry.ls_recv_ms = Some(latency_ms),
        }

        if let (Some(ys), Some(ls)) = (entry.ys_recv_ms, entry.ls_recv_ms) {
            let delta = ys - ls; // positive => YS arrived later
            println!("{slot},{ys},{ls},{delta}");
            
            deltas.push(delta);
            count += 1;
            
            if count % 100 == 0 {
                print_statistics(&deltas, count);
            }
            
            timings.remove(&slot);
        }
    }

    Ok(())
}

async fn handle_update(
    source: Source,
    update: SubscribeUpdate,
    agg_tx: &mpsc::UnboundedSender<(Source, u64, i128)>,
) {
    if let Some(UpdateOneof::BlockMeta(block_meta)) = update.update_oneof {
        let slot = block_meta.slot;
        let recv_ms = system_time_to_millis(SystemTime::now());
        debug!(?source, slot, recv_ms, "BlockMeta received");
        let _ = agg_tx.send((source, slot, recv_ms));
    }
}

fn print_statistics(deltas: &[i128], count: usize) {
    if deltas.is_empty() {
        return;
    }
    
    let mut sorted_deltas = deltas.to_vec();
    sorted_deltas.sort();
    
    let median = if sorted_deltas.len() % 2 == 0 {
        let mid = sorted_deltas.len() / 2;
        (sorted_deltas[mid - 1] + sorted_deltas[mid]) / 2
    } else {
        sorted_deltas[sorted_deltas.len() / 2]
    };
    
    let min = *sorted_deltas.first().unwrap();
    let max = *sorted_deltas.last().unwrap();
    let sum: i128 = sorted_deltas.iter().sum();
    let mean = sum / sorted_deltas.len() as i128;
    
    let p25_idx = (sorted_deltas.len() as f64 * 0.25) as usize;
    let p75_idx = (sorted_deltas.len() as f64 * 0.75) as usize;
    let p95_idx = (sorted_deltas.len() as f64 * 0.95) as usize;
    
    let p25 = sorted_deltas[p25_idx.min(sorted_deltas.len() - 1)];
    let p75 = sorted_deltas[p75_idx.min(sorted_deltas.len() - 1)];
    let p95 = sorted_deltas[p95_idx.min(sorted_deltas.len() - 1)];
    
    eprintln!("--- Statistics after {} slots ---", count);
    eprintln!("Delta (YS - LS) in milliseconds:");
    eprintln!("  Min: {}, Max: {}", min, max);
    eprintln!("  Mean: {}, Median: {}", mean, median);
    eprintln!("  P25: {}, P75: {}, P95: {}", p25, p75, p95);
    eprintln!("  Positive deltas (YS slower): {}/{} ({:.1}%)", 
              sorted_deltas.iter().filter(|&&x| x > 0).count(),
              sorted_deltas.len(),
              sorted_deltas.iter().filter(|&&x| x > 0).count() as f64 / sorted_deltas.len() as f64 * 100.0);
    eprintln!("---");
}

fn system_time_to_millis(st: SystemTime) -> i128 {
    st.duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_millis() as i128
}
```

**Hal yang diukur:** Perbedaan performa relatif antara dua layanan streaming. Delta menunjukkan layanan mana yang mengirimkan informasi slot yang sama terlebih dahulu.

**Metrik utama:**

* **Delta positif**: Layanan pertama (YS) lebih lambat daripada layanan kedua (LS)—LaserStream lebih cepat
* **Delta negatif**: Layanan pertama (YS) lebih cepat daripada layanan kedua (LS)—LaserStream lebih lambat
* **Rata-rata/Median**: Perbedaan performa rata-rata
* **P95**: Perbedaan latensi persentil ke-95

**Menjalankan pengujian:**

```bash theme={"system"}
cargo run --bin latency-comparison
```

**Contoh keluaran:**

```
slot,ys_recv_ms,ls_recv_ms,delta_ms
352416939,1752168399141,1752168399140,1
352416940,1752168399526,1752168399512,14
352416941,1752168399890,1752168399877,13
```

Keluaran menampilkan perbedaan latensi secara waktu nyata dan statistik berkala. Delta rata-rata yang positif menunjukkan bahwa layanan kedua (LaserStream) secara konsisten mengirimkan data lebih cepat.

***

## Metode 2: Analisis stempel waktu pembuatan

Pendekatan ini membandingkan stempel waktu `created_at` yang disematkan dalam pesan dengan waktu sistem lokal Anda saat pesan diterima.

```rust [expandable] theme={"system"}
use std::time::{Duration, SystemTime};
use dotenvy::dotenv;
use tracing::{debug, error, info};
use yellowstone_grpc_proto::prost_types::Timestamp;
use futures::StreamExt;
use futures::SinkExt;
use std::collections::HashMap;

use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient};
use yellowstone_grpc_proto::prelude::{
    subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest,
    SubscribeRequestFilterTransactions, SubscribeUpdate,
};

const ACCOUNTS_INCLUDE: &[&str] = &["BB5dnY55FXS1e1NXqZDwCzgdYJdMCj3B92PU6Q5Fb6DT"];
const COMMITMENT_LEVEL: &str = "processed";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _ = dotenv();
    tracing_subscriber::fmt().with_env_filter("info").init();

    let grpc_url = std::env::var("YS_GRPC_URL").expect("GRPC_URL env variable not set");
    let api_key = std::env::var("YS_API_KEY").ok();

    info!("Connecting to {} …", grpc_url);
    debug!(accounts_include = ?ACCOUNTS_INCLUDE, "Subscribing with accountsInclude filter");

    let mut client = GeyserGrpcClient::build_from_shared(grpc_url.clone())
        .expect("invalid URL")
        .x_token(api_key.clone())?
        .tls_config(ClientTlsConfig::new().with_native_roots())?
        .connect()
        .await?;

    let (mut subscribe_tx, mut subscribe_rx) = client.subscribe().await?;

    let mut tx_filter_map = HashMap::new();
    tx_filter_map.insert(
        "latency".to_string(),
        SubscribeRequestFilterTransactions {
            account_include: ACCOUNTS_INCLUDE.iter().map(|s| s.to_string()).collect(),
            vote: Some(false),
            failed: Some(false),
            ..Default::default()
        },
    );

    subscribe_tx
        .send(SubscribeRequest {
            transactions: tx_filter_map,
            commitment: Some(CommitmentLevel::Processed as i32),
            ..Default::default()
        })
        .await?;

    let mut latencies: Vec<i64> = Vec::new();
    let mut count = 0;

    while let Some(update_res) = subscribe_rx.next().await {
        match update_res {
            Ok(update) => {
                if let Some(UpdateOneof::Transaction(tx)) = update.update_oneof {
                    let recv_time = SystemTime::now();
                    
                    if let Some(created_at) = update.created_at {
                        let created_time = SystemTime::UNIX_EPOCH + Duration::new(
                            created_at.seconds as u64,
                            created_at.nanos as u32,
                        );
                        
                        if let Ok(latency) = recv_time.duration_since(created_time) {
                            let latency_ms = latency.as_millis() as i64;
                            latencies.push(latency_ms);
                            count += 1;
                            
                            println!("Transaction latency: {}ms", latency_ms);
                            
                            if count % 100 == 0 {
                                print_statistics(&latencies, count);
                            }
                        }
                    }
                }
            }
            Err(e) => {
                error!("Stream error: {:?}", e);
                break;
            }
        }
    }

    Ok(())
}

fn print_statistics(latencies: &[i64], count: usize) {
    if latencies.is_empty() {
        return;
    }
    
    let mut sorted = latencies.to_vec();
    sorted.sort();
    
    let median = if sorted.len() % 2 == 0 {
        let mid = sorted.len() / 2;
        (sorted[mid - 1] + sorted[mid]) / 2
    } else {
        sorted[sorted.len() / 2]
    };
    
    let min = *sorted.first().unwrap();
    let max = *sorted.last().unwrap();
    let sum: i64 = sorted.iter().sum();
    let mean = sum / sorted.len() as i64;
    
    let p95_idx = (sorted.len() as f64 * 0.95) as usize;
    let p95 = sorted[p95_idx.min(sorted.len() - 1)];
    
    eprintln!("--- Statistics after {} transactions ---", count);
    eprintln!("Latency (created_at to receive) in milliseconds:");
    eprintln!("  Min: {}, Max: {}", min, max);
    eprintln!("  Mean: {}, Median: {}", mean, median);
    eprintln!("  P95: {}", p95);
    eprintln!("---");
}
```

**Keterbatasan penting:** Metode ini hanya mengukur waktu sejak LaserStream membuat pesan hingga Anda menerimanya. Metode ini tidak memperhitungkan penundaan upstream antara peristiwa blockchain dan pemrosesan LaserStream.

**Menjalankan pengujian:**

```bash theme={"system"}
cargo run --bin timestamp-analysis
```

**Contoh keluaran:**

```
Transaction latency: 45ms
Transaction latency: 52ms
Transaction latency: 38ms
--- Statistics after 100 transactions ---
Latency (created_at to receive) in milliseconds:
  Min: 28, Max: 89
  Mean: 47, Median: 45
  P95: 72
---
```

Metode ini memberikan wawasan mengenai latensi jaringan dan pemrosesan antara LaserStream dan aplikasi Anda, tetapi sebaiknya digunakan bersama dengan Metode 1 untuk analisis yang komprehensif.

***

## Praktik terbaik untuk pengujian latensi

### Prinsip utama

* **Kolokasi**: Terapkan pengujian di wilayah yang sama dengan endpoint LaserStream Anda untuk meminimalkan latensi jaringan
* **Beberapa metode**: Gunakan perbandingan aliran paralel (Metode 1) sebagai metrik utama Anda dan lengkapi dengan analisis stempel waktu
* **Pemantauan jangka panjang**: Jalankan pengujian dalam waktu lama untuk menangkap berbagai kondisi jaringan dan kepadatan blockchain
* **Analisis statistik**: Fokus pada persentil (P95, P99), bukan hanya rata-rata, untuk memahami latensi ekor

### Menafsirkan hasil

1. **Tetapkan tolok ukur awal**: Jalankan pengujian setidaknya selama 1 jam untuk menetapkan performa dasar dalam kondisi normal
2. **Identifikasi pola**: Cari pola dalam lonjakan latensi—apakah lonjakan tersebut berkorelasi dengan aktivitas blockchain yang tinggi atau kepadatan jaringan?
3. **Bandingkan persentil**: Latensi P95 sering kali lebih penting daripada latensi rata-rata bagi pengalaman pengguna
4. **Pantau konsistensi**: Performa yang konsisten sering kali lebih berharga daripada latensi minimum absolut

Ingat bahwa latensi blockchain pada dasarnya bervariasi karena persyaratan konsensus jaringan. Fokus pada perbedaan performa relatif dan konsistensi, bukan angka absolut.
