> ## 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**: самый быстрый, подтверждение одним валидатором (\~400 мс)
* **Confirmed**: средний, подтверждение квалифицированным большинством (\~2–3 секунды)
* **Finalized**: самый медленный, полная финализация сетью (\~15–30 секунд)

Для приложений, чувствительных к задержке, обычно выбирают уровень подтверждения **processed**. Во всех тестах этого руководства используется уровень processed, поскольку в большинстве высокочастотных сценариев скорость важнее абсолютной финальности.

## Три подхода к измерению задержки

### 1. Сравнение параллельных потоков gRPC

**Самый надежный метод** — сравнивает два независимых потока из одного источника данных и определяет, какой из них первым получает идентичные события.

**Преимущества:**

* Устраняет проблемы синхронизации часов
* Позволяет сравнивать относительную производительность
* Обеспечивает наиболее точное сравнение сервисов

### 2. Сравнение локальной временной метки с created\_at

**Средняя надежность** — измеряет разницу между моментом получения сообщения вашей системой и временной меткой, добавленной в сообщение сервисом LaserStream.

**Ограничения:**

* Отражает только момент внутреннего создания сообщения в LaserStream
* Не учитывает задержки на пути к LaserStream
* Менее точен для измерения реальной сквозной задержки, чем метод 1

### 3. Анализ временной метки блока (не рекомендуется)

**Не рекомендуется** — сравнивает локальное время получения с временной меткой блока Solana.

**Существенные ограничения:**

* Временные метки блоков имеют точность только до секунды
* Solana создает блоки каждые 400 мс
* Предоставляет минимум полезной информации

***

## Требования к настройке

### Совместное размещение в одном регионе

Чтобы получить значимые результаты измерения задержки, разверните тестовую инфраструктуру в том же ЦОД или регионе, где находится конечная точка 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`

Полные инструкции по настройке и рекомендации по выбору конечной точки см. в [документации по gRPC LaserStream](/docs/ru/laserstream/grpc).

### Настройка среды Rust

Все скрипты измерения используют Rust и Cargo. Базовая настройка:

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

Создайте файл `.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
```

Получите ключ API Helius в [панели управления Helius](https://dashboard.helius.dev/). LaserStream devnet доступен на всех тарифах. Для доступа к mainnet требуется тариф Business или Professional.

***

## Метод 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. **Контролируйте стабильность**: стабильная производительность часто важнее абсолютного минимума задержки

Помните, что задержка блокчейна по своей природе изменчива из-за требований к достижению сетевого консенсуса. Уделяйте основное внимание относительным различиям в производительности и стабильности, а не абсолютным значениям.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.