Skip to main content

linera_client/
client_metrics.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use hdrhistogram::Histogram;
5use linera_core::client::TimingType;
6use tokio::{sync::mpsc, task, time};
7use tracing::{debug, info, warn};
8
9#[derive(Debug, Clone)]
10/// Configuration for client-side timing metrics collection and reporting.
11pub struct TimingConfig {
12    /// Whether timing metrics collection is enabled.
13    pub enabled: bool,
14    /// How often, in seconds, the collected timing data is reported.
15    pub report_interval_secs: u64,
16}
17
18#[cfg(not(web))]
19impl Default for TimingConfig {
20    fn default() -> Self {
21        Self {
22            enabled: false,
23            report_interval_secs: 5,
24        }
25    }
26}
27
28#[derive(Debug, thiserror::Error)]
29/// Errors that can occur while creating or recording client timing metrics.
30pub enum ClientMetricsError {
31    /// A histogram could not be created.
32    #[error("Failed to create histogram: {0}")]
33    HistogramCreationError(#[from] hdrhistogram::CreationError),
34    /// A value could not be recorded into a histogram.
35    #[error("Failed to record histogram: {0}")]
36    HistogramRecordError(#[from] hdrhistogram::RecordError),
37}
38
39/// Histograms tracking the time spent in the individual phases of executing a block.
40pub struct ExecuteBlockTimingsHistograms {
41    /// Time taken to submit a block proposal, in milliseconds.
42    pub submit_block_proposal_histogram: Histogram<u64>,
43    /// Time taken to update the validators, in milliseconds.
44    pub update_validators_histogram: Histogram<u64>,
45}
46
47impl ExecuteBlockTimingsHistograms {
48    /// Creates a new set of block-phase timing histograms.
49    pub fn new() -> Result<Self, ClientMetricsError> {
50        Ok(Self {
51            submit_block_proposal_histogram: Histogram::<u64>::new(2)?,
52            update_validators_histogram: Histogram::<u64>::new(2)?,
53        })
54    }
55}
56
57/// Histograms tracking the time spent executing operations, broken down by sub-phase.
58pub struct ExecuteOperationsTimingsHistograms {
59    /// Time taken to execute a single block, in milliseconds.
60    pub execute_block_histogram: Histogram<u64>,
61    /// Nested histograms for the block-execution sub-phases.
62    pub execute_block_timings_histograms: ExecuteBlockTimingsHistograms,
63}
64
65impl ExecuteOperationsTimingsHistograms {
66    /// Creates a new set of operation-execution timing histograms.
67    pub fn new() -> Result<Self, ClientMetricsError> {
68        Ok(Self {
69            execute_block_histogram: Histogram::<u64>::new(2)?,
70            execute_block_timings_histograms: ExecuteBlockTimingsHistograms::new()?,
71        })
72    }
73}
74
75/// Histograms tracking the time spent processing a whole block, broken down by phase.
76pub struct BlockTimingsHistograms {
77    /// Time taken to execute the operations in a block, in milliseconds.
78    pub execute_operations_histogram: Histogram<u64>,
79    /// Nested histograms for the operation-execution sub-phases.
80    pub execute_operations_timings_histograms: ExecuteOperationsTimingsHistograms,
81}
82
83impl BlockTimingsHistograms {
84    /// Creates a new set of block timing histograms.
85    pub fn new() -> Result<Self, ClientMetricsError> {
86        Ok(Self {
87            execute_operations_histogram: Histogram::<u64>::new(2)?,
88            execute_operations_timings_histograms: ExecuteOperationsTimingsHistograms::new()?,
89        })
90    }
91
92    /// Records a measured duration into the histogram matching the given timing type.
93    pub fn record_timing(
94        &mut self,
95        duration_ms: u64,
96        timing_type: TimingType,
97    ) -> Result<(), ClientMetricsError> {
98        match timing_type {
99            TimingType::ExecuteOperations => {
100                self.execute_operations_histogram.record(duration_ms)?;
101            }
102            TimingType::ExecuteBlock => {
103                self.execute_operations_timings_histograms
104                    .execute_block_histogram
105                    .record(duration_ms)?;
106            }
107            TimingType::SubmitBlockProposal => {
108                self.execute_operations_timings_histograms
109                    .execute_block_timings_histograms
110                    .submit_block_proposal_histogram
111                    .record(duration_ms)?;
112            }
113            TimingType::UpdateValidators => {
114                self.execute_operations_timings_histograms
115                    .execute_block_timings_histograms
116                    .update_validators_histogram
117                    .record(duration_ms)?;
118            }
119        }
120        Ok(())
121    }
122}
123
124/// Collects client-side timing metrics and reports them periodically.
125#[cfg(not(web))]
126pub struct ClientMetrics {
127    /// Configuration controlling whether timing collection is enabled and how often it reports.
128    pub timing_config: TimingConfig,
129    /// Sender used to forward measured durations and their timing type to the collection task.
130    pub timing_sender: mpsc::UnboundedSender<(u64, TimingType)>,
131    /// Handle to the background task that aggregates and reports timing data.
132    pub timing_task: task::JoinHandle<()>,
133}
134
135#[cfg(not(web))]
136impl ClientMetrics {
137    /// Creates a new client metrics collector and spawns its background reporting task.
138    pub fn new(timing_config: TimingConfig) -> Self {
139        let (tx, rx) = mpsc::unbounded_channel();
140        let timing_task = tokio::spawn(Self::timing_collection(
141            rx,
142            timing_config.report_interval_secs,
143        ));
144
145        Self {
146            timing_config,
147            timing_sender: tx,
148            timing_task,
149        }
150    }
151
152    async fn timing_collection(
153        mut receiver: mpsc::UnboundedReceiver<(u64, TimingType)>,
154        report_interval_secs: u64,
155    ) {
156        let mut histograms =
157            BlockTimingsHistograms::new().expect("Failed to create timing histograms");
158
159        let mut report_needed = false;
160        let mut report_timer = time::interval(time::Duration::from_secs(report_interval_secs));
161        report_timer.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
162
163        loop {
164            tokio::select! {
165                timing_data = receiver.recv() => {
166                    match timing_data {
167                        Some((duration_ms, timing_type)) => {
168                            if let Err(e) = histograms.record_timing(duration_ms, timing_type) {
169                                warn!("Failed to record timing data: {}", e);
170                            } else {
171                                report_needed = true;
172                            }
173                        }
174                        None => {
175                            debug!("Timing collection task shutting down - sender closed");
176                            break;
177                        }
178                    }
179                }
180                _ = report_timer.tick() => {
181                    if report_needed {
182                        Self::print_timing_report(&histograms);
183                        report_needed = false;
184                    }
185                }
186            }
187        }
188    }
189
190    #[expect(
191        clippy::cast_possible_truncation,
192        clippy::cast_sign_loss,
193        reason = "quantile is a fixed small positive value used for display"
194    )]
195    fn print_timing_report(histograms: &BlockTimingsHistograms) {
196        for quantile in [0.99, 0.95, 0.90, 0.50] {
197            let formatted_quantile = (quantile * 100.0) as usize;
198
199            info!(
200                "Execute operations p{}: {} ms",
201                formatted_quantile,
202                histograms
203                    .execute_operations_histogram
204                    .value_at_quantile(quantile)
205            );
206
207            info!(
208                "  └─ Execute block p{}: {} ms",
209                formatted_quantile,
210                histograms
211                    .execute_operations_timings_histograms
212                    .execute_block_histogram
213                    .value_at_quantile(quantile)
214            );
215            info!(
216                "    ├─ Submit block proposal p{}: {} ms",
217                formatted_quantile,
218                histograms
219                    .execute_operations_timings_histograms
220                    .execute_block_timings_histograms
221                    .submit_block_proposal_histogram
222                    .value_at_quantile(quantile)
223            );
224            info!(
225                "    └─ Update validators p{}: {} ms",
226                formatted_quantile,
227                histograms
228                    .execute_operations_timings_histograms
229                    .execute_block_timings_histograms
230                    .update_validators_histogram
231                    .value_at_quantile(quantile)
232            );
233        }
234    }
235}