> ## 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.

# 지연 시간 측정

> 다양한 테스트 방법을 사용하여 gRPC 스트림의 지연 시간을 올바르게 측정하고 분석하는 방법을 배우십시오.

<Warning>
  **중요한 면책 조항: 프로덕션 데이터 센터 전용**

  이 지연 시간 테스트는 **프로덕션 데이터 센터 환경 전용**으로 설계되었습니다. 이러한 테스트를 로컬 머신이나 소비자 인터넷 연결에서 실행하지 마십시오. 로컬 대역폭은 Solana 구독을 많이 처리할 수 없으며 현실 세계의 성능을 반영하지 않는 의미 없는 결과를 생성할 것입니다.
</Warning>

<Warning>
  **동일 위치 배치 요구 사항: LaserStream 엔드포인트 근처 배포**

  의미 있는 지연 시간 측정을 위해 선택한 LaserStream 엔드포인트와 동일한 지역에 테스트 인프라를 **반드시** 공동 배치해야 합니다. 네트워크 거리가 측정의 대부분을 차지하므로 다른 대륙에서 테스트하면 LaserStream 성능이 아닌 네트워크 지연 시간이 표시됩니다.
</Warning>

***

## 분산 블록체인 시스템에서의 지연 시간 이해

블록체인 스트리밍 서비스를 사용할 때, 분산 시스템에는 보편적인 시계가 없기 때문에 지연 시간 측정이 복잡해집니다. 전통적인 시스템에서는 단일 서버까지의 왕복 시간을 측정할 수 있지만, 블록체인 네트워크는 여러 검증자가 관여하며, 각 검증자가 다른 시점에 동일한 트랜잭션을 수신 및 처리합니다.

**기본적인 도전 과제:** Solana와 같은 블록체인은 절대 시간이라는 개념이 없습니다. 각 검증자 노드는 전 세계적으로 동일한 트랜잭션을 다른 시점에 수신하게 되며, 확인은 클러스터의 일정 비율이 합의에 도달해야 합니다. 따라서 전통적인 의미에서 결정론적인 지연 시간 측정은 불가능합니다.

## 커밋 수준 및 지연 시간 우선순위

Solana는 각각 다른 지연 시간 특성을 가진 세 가지 커밋 수준을 제공합니다:

* **처리됨(Processed)**: 가장 빠름, 단일 검증자 확인 (\~400ms)
* **확인됨(Confirmed)**: 중간, 슈퍼다수 확인 (\~2-3초)
* **완료됨(Finalized)**: 가장 느림, 네트워크 완전 최종화 (\~15-30초)

지연 시간에 민감한 애플리케이션의 경우 **처리된 커밋**이 일반적으로 목표입니다. 이 가이드의 모든 테스트는 처리된 커밋 수준을 사용하며 대부분의 고주파수 사용 사례는 절대적 최종성보다 속도를 우선시합니다.

## 지연 시간 측정의 세 가지 접근 방식

### 1. 병렬 gRPC 스트림 비교

**가장 신뢰할 수 있는 방법** - 동일한 데이터 소스에 대한 두 개의 독립된 스트림을 비교하여 동일한 이벤트를 먼저 수신하는지를 측정합니다.

**장점:**

* 시계 동기화 문제 제거
* 상대적 성능 비교 제공
* 서비스 비교에 가장 정확

### 2. 로컬 타임스탬프 vs created\_at 비교

**중간 신뢰성** - 시스템이 메시지를 수신한 시점과 LaserStream 서비스에 의해 메시지에 내장된 타임스탬프 간의 차이를 측정합니다.

**제한사항:**

* LaserStream이 내부적으로 메시지를 생성한 시점을 나타냅니다
* LaserStream에 대한 업스트림 지연이 포착되지 않음
* 진정한 종단 간 지연 시간에 대한 정확성이 방법 1보다 낮음

### 3. 블록 타임스탬프 분석 (권장하지 않음)

**권장하지 않음** - Solana의 블록 타임스탬프와 로컬 수신 시간을 비교합니다.

**중대한 제한사항:**

* 블록 타임스탬프는 초 단위의 정밀도만 가짐
* Solana는 매 400ms마다 블록을 생성
* 최소한의 유용한 정보 제공

***

## 설정 요구 사항

### 지역 동시 배치

의미 있는 지연 시간 측정을 위해 LaserStream 엔드포인트와 동일 데이터 센터 또는 지역에 테스트 인프라를 배포하십시오.

**이용 가능한 LaserStream 지역:**

* **ewr**: 미국 뉴욕 (동부 해안) - `https://laserstream-mainnet-ewr.helius-rpc.com`
* **pitt**: 미국 피츠버그 (중부) - `https://laserstream-mainnet-pitt.helius-rpc.com`
* **slc**: 미국 솔트레이크시티 (서부 해안) - `https://laserstream-mainnet-slc.helius-rpc.com`
* **ams**: 네덜란드 암스테르담, 유럽 - `https://laserstream-mainnet-ams.helius-rpc.com`
* **fra**: 독일 프랑크푸르트, 유럽 - `https://laserstream-mainnet-fra.helius-rpc.com`
* **tyo**: 일본 도쿄, 아시아 - `https://laserstream-mainnet-tyo.helius-rpc.com`
* **sgp**: 싱가포르, 아시아 - `https://laserstream-mainnet-sgp.helius-rpc.com`

devnet 테스트를 위해, 다음을 사용하십시오: `https://laserstream-devnet-ewr.helius-rpc.com`

완벽한 설정 지침과 엔드포인트 선택 가이드라인은 [LaserStream gRPC 문서](/docs/ko/laserstream/grpc)를 참조하십시오.

### Rust 환경 설정

모든 측정 스크립트는 Cargo와 함께 Rust를 사용합니다. 기본 설정:

```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 = "8.0.0"
yellowstone-grpc-proto = "8.0.0"
# For timestamp analysis
prost-types = "0.12"
```

자격 증명을 포함한 `.env` 파일을 만드십시오:

```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
```

[Helius 대시보드](https://dashboard.helius.dev/)에서 Helius API 키를 가져오세요. 모든 플랜에서 LaserStream devnet을 사용할 수 있습니다. 메인넷 액세스는 비즈니스 또는 프로페셔널 플랜이 필요합니다.

***

## 방법 1: 병렬 스트림 비교

이 스크립트는 다른 gRPC 엔드포인트에 두 개의 독립된 연결을 설정하고 동일한 `BlockMeta` 메시지를 먼저 수신하는지를 측정합니다. 이 접근 방식은 상대적 타이밍을 사용하여 시계 동기화 문제를 제거합니다.

```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
}
```

**측정 대상:** 두 스트리밍 서비스 간의 상대적 성능 차이를 측정합니다. 델타는 동일한 슬롯 정보를 먼저 전달하는 서비스를 보여줍니다.

**주요 메트릭:**

* **양수 델타**: 첫 번째 서비스 (YS)가 두 번째 서비스 (LS)보다 느림 - LaserStream이 더 빠름
* **음수 델타**: 첫 번째 서비스 (YS)가 두 번째 서비스 (LS)보다 빠름 - LaserStream이 느림
* **평균/중앙값**: 평균 성능 차이
* **P95**: 95번째 백분위 지연 시간 차이

**테스트 실행:**

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

**샘플 출력:**

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

출력은 실시간 지연 시간 차이와 주기적인 통계를 보여줍니다. 양의 평균 델타는 두 번째 서비스 (LaserStream)가 일관되게 데이터를 더 빠르게 전달함을 나타냅니다.

***

## 방법 2: 생성된 타임스탬프 분석

이 접근 방식은 메시지에 포함된 `created_at` 타임스탬프와 수신 시 로컬 시스템 시간과 비교합니다.

```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!("---");
}
```

**중요한 제한사항:** 이 방법은 LaserStream이 메시지를 생성한 시점부터 귀하가 수신한 시점까지만 측정합니다. 블록체인 이벤트와 LaserStream 처리 간의 업스트림 지연은 고려되지 않습니다.

**테스트 실행:**

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

**샘플 출력:**

```
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
---
```

이 방법은 LaserStream과 애플리케이션 간의 네트워크와 처리 지연 시간에 대한 통찰력을 제공하지만 포괄적인 분석을 위해 방법 1과 함께 사용해야 합니다.

***

## 지연 시간 테스트를 위한 모범 사례

### 주요 원칙

* **동일 위치 배치**: 네트워크 지연 시간을 최소화하기 위해 LaserStream 엔드포인트와 동일 지역에 테스트를 배포하십시오
* **여러 방법 사용**: 병렬 스트림 비교 방법(방법 1)을 기본 메트릭으로 사용하고 타임스탬프 분석으로 보완하십시오
* **장기 모니터링**: 다양한 네트워크 조건과 블록체인 혼잡을 포착하기 위해 테스트를 장기간 수행하십시오
* **통계 분석**: 평균뿐만 아니라 백분위수(P95, P99)에 집중하여 꼬리 지연 시간을 이해하십시오

### 결과 해석

1. **기준 수립**: 정상 상태에서 성능 기준을 수립하기 위해 최소 1시간 동안 테스트 실행
2. **패턴 식별**: 지연 시간 급증 패턴 조사 - 높은 블록체인 활동이나 네트워크 혼잡과 상관관계가 있는가?
3. **백분위수 비교**: 사용자 경험을 위해 P95 지연 시간이 평균 지연 시간보다 더 중요할 때가 많음
4. **일관성 모니터링**: 일관된 성능이 절대적인 최소 지연 시간보다 더 가치 있을 때가 많음

블록체인 지연 시간은 네트워크 합의 요구 사항 때문에 본질적으로 가변적입니다. 절대적인 숫자보다 상대적 성능 차이와 일관성에 집중하십시오.
