linera_client/
client_metrics.rs1use hdrhistogram::Histogram;
5use linera_core::client::TimingType;
6use tokio::{sync::mpsc, task, time};
7use tracing::{debug, info, warn};
8
9#[derive(Debug, Clone)]
10pub struct TimingConfig {
12 pub enabled: bool,
14 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)]
29pub enum ClientMetricsError {
31 #[error("Failed to create histogram: {0}")]
33 HistogramCreationError(#[from] hdrhistogram::CreationError),
34 #[error("Failed to record histogram: {0}")]
36 HistogramRecordError(#[from] hdrhistogram::RecordError),
37}
38
39pub struct ExecuteBlockTimingsHistograms {
41 pub submit_block_proposal_histogram: Histogram<u64>,
43 pub update_validators_histogram: Histogram<u64>,
45}
46
47impl ExecuteBlockTimingsHistograms {
48 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
57pub struct ExecuteOperationsTimingsHistograms {
59 pub execute_block_histogram: Histogram<u64>,
61 pub execute_block_timings_histograms: ExecuteBlockTimingsHistograms,
63}
64
65impl ExecuteOperationsTimingsHistograms {
66 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
75pub struct BlockTimingsHistograms {
77 pub execute_operations_histogram: Histogram<u64>,
79 pub execute_operations_timings_histograms: ExecuteOperationsTimingsHistograms,
81}
82
83impl BlockTimingsHistograms {
84 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 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#[cfg(not(web))]
126pub struct ClientMetrics {
127 pub timing_config: TimingConfig,
129 pub timing_sender: mpsc::UnboundedSender<(u64, TimingType)>,
131 pub timing_task: task::JoinHandle<()>,
133}
134
135#[cfg(not(web))]
136impl ClientMetrics {
137 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}