Skip to main content

futu_core/
delay_stats.rs

1//! C++ `INNData_ProtoDelay`-style local delay statistics.
2//!
3//! The request/reply statistic needs to observe both public API dispatch and
4//! backend request spans.  Public API dispatch is wired from `futu-server`,
5//! while backend spans are wired in `BackendConn::request_with_reserved_timeout`.
6//! Keeping this store in `futu-core` avoids a dependency from `futu-server` back
7//! to the whole backend crate while preserving one process-wide stats store.
8
9use std::collections::HashMap;
10use std::future::Future;
11use std::sync::Arc;
12use std::sync::LazyLock;
13use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
14
15use parking_lot::Mutex;
16
17mod qot_push;
18mod response;
19
20use qot_push::QotPushCounter;
21use response::response_ret_type_is_success;
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct ReqReplyStatisticsSnapshot {
25    pub proto_id: u32,
26    pub count: u32,
27    pub total_cost_avg_ms: f32,
28    pub open_d_cost_avg_ms: f32,
29    pub net_delay_avg_ms: f32,
30    pub is_local_reply: bool,
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub struct QotPushStatisticsSnapshot {
35    pub qot_push_type: i32,
36    pub item_list: Vec<DelayStatisticsItemSnapshot>,
37    pub delay_avg_ms: f32,
38    pub count: i32,
39}
40
41#[derive(Debug, Clone, PartialEq)]
42pub struct PlaceOrderStatisticsSnapshot {
43    pub order_id: String,
44    pub total_cost_ms: f32,
45    pub open_d_cost_ms: f32,
46    pub net_delay_ms: f32,
47    pub update_cost_ms: f32,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct DelayStatisticsItemSnapshot {
52    pub begin: i32,
53    pub end: i32,
54    pub count: i32,
55    pub proportion: f32,
56    pub cumulative_ratio: f32,
57}
58
59pub const QOT_PUSH_TYPE_PRICE: i32 = 1;
60pub const QOT_PUSH_TYPE_TICKER: i32 = 2;
61pub const QOT_PUSH_TYPE_ORDER_BOOK: i32 = 3;
62pub const QOT_PUSH_TYPE_BROKER: i32 = 4;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum QotPushDelayRecordOutcome {
66    Recorded,
67    SkippedByCountGate,
68    SkippedByNotCalDelay,
69}
70
71#[derive(Debug, Clone, Copy)]
72pub struct QotPushDelaySample {
73    pub count_delay: bool,
74    pub not_cal_delay: Option<i32>,
75    pub qot_push_type: i32,
76    pub server_recv_from_exchange_time_ms: Option<i64>,
77    pub server_send_to_client_time_ms: Option<i64>,
78    pub f3c_recv_at: SystemTime,
79    pub api_response_at: SystemTime,
80}
81
82const QOT_PUSH_STAGE_SR2SS: i32 = 1;
83const QOT_PUSH_STAGE_SS2CR: i32 = 2;
84const QOT_PUSH_STAGE_CR2CS: i32 = 3;
85const QOT_PUSH_STAGE_SS2CS: i32 = 4;
86const QOT_PUSH_STAGE_SR2CS: i32 = 5;
87
88#[derive(Default)]
89struct ReqReplyCounter {
90    count: u32,
91    total_cost_avg_ms: f64,
92    open_d_cost_avg_ms: f64,
93    net_delay_avg_ms: f64,
94    is_local_reply: bool,
95}
96
97impl ReqReplyCounter {
98    fn add(&mut self, total: Duration, open_d: Duration, net_delay: Duration, local_reply: bool) {
99        let count = self.count as f64;
100        self.total_cost_avg_ms =
101            ((self.total_cost_avg_ms * count) + duration_ms(total)) / (count + 1.0);
102        self.open_d_cost_avg_ms =
103            ((self.open_d_cost_avg_ms * count) + duration_ms(open_d)) / (count + 1.0);
104        self.net_delay_avg_ms =
105            ((self.net_delay_avg_ms * count) + duration_ms(net_delay)) / (count + 1.0);
106        self.count += 1;
107        if local_reply {
108            self.is_local_reply = true;
109        }
110    }
111
112    fn snapshot(&self, proto_id: u32) -> ReqReplyStatisticsSnapshot {
113        ReqReplyStatisticsSnapshot {
114            proto_id,
115            count: self.count,
116            total_cost_avg_ms: self.total_cost_avg_ms as f32,
117            open_d_cost_avg_ms: self.open_d_cost_avg_ms as f32,
118            net_delay_avg_ms: self.net_delay_avg_ms as f32,
119            is_local_reply: self.is_local_reply,
120        }
121    }
122}
123
124#[derive(Default)]
125struct DelayStatisticsStore {
126    req_reply_counts: Mutex<HashMap<u32, ReqReplyCounter>>,
127    qot_push_counts: Mutex<HashMap<i32, QotPushCounter>>,
128    place_order_req_details: Mutex<Vec<PlaceOrderReqDetail>>,
129    place_order_push_once: Mutex<Vec<PlaceOrderPushOnce>>,
130    time_adjustment: Mutex<TimeAdjustment>,
131}
132
133impl DelayStatisticsStore {
134    fn set_time_adjustment(&self, s2c_time_diff_us: i64, net_delay_us: i64) {
135        *self.time_adjustment.lock() = TimeAdjustment {
136            s2c_time_diff_us,
137            net_delay_us,
138        };
139    }
140
141    fn time_adjustment(&self) -> TimeAdjustment {
142        *self.time_adjustment.lock()
143    }
144
145    fn record_req_reply(&self, ctx: &ApiRequestDelayContext) {
146        let total_cost = ctx.begin.elapsed();
147        let api_end_at = SystemTime::now();
148        let backend_spans = ctx.backend_spans.lock();
149        let is_local_reply = backend_spans.is_empty();
150        let backend_cost = backend_union_duration(&backend_spans);
151        let open_d_cost = total_cost.saturating_sub(backend_cost);
152
153        self.req_reply_counts
154            .lock()
155            .entry(ctx.proto_id)
156            .or_default()
157            .add(total_cost, open_d_cost, Duration::ZERO, is_local_reply);
158
159        self.record_place_order_req_detail(ctx, &backend_spans, api_end_at);
160    }
161
162    fn snapshot_req_reply(&self) -> Vec<ReqReplyStatisticsSnapshot> {
163        self.snapshot_req_reply_with(|| {})
164    }
165
166    fn snapshot_req_reply_with(&self, hook: impl FnOnce()) -> Vec<ReqReplyStatisticsSnapshot> {
167        let counts = self.req_reply_counts.lock();
168        hook();
169        let mut items: Vec<_> = counts
170            .iter()
171            .map(|(&proto_id, counter)| counter.snapshot(proto_id))
172            .collect();
173        items.sort_by_key(|item| item.proto_id);
174        items
175    }
176
177    #[cfg(test)]
178    fn record_req_reply_sample_for_test(&self, proto_id: u32, total: Duration) {
179        self.req_reply_counts
180            .lock()
181            .entry(proto_id)
182            .or_default()
183            .add(total, total, Duration::ZERO, true);
184    }
185
186    #[cfg(test)]
187    fn snapshot_req_reply_with_test_hook(
188        &self,
189        hook: impl FnOnce(),
190    ) -> Vec<ReqReplyStatisticsSnapshot> {
191        self.snapshot_req_reply_with(hook)
192    }
193
194    fn record_qot_push_count(
195        &self,
196        qot_push_type: i32,
197        server_recv_from_exchange_time_ms: Option<i64>,
198        server_send_to_client_time_ms: Option<i64>,
199        f3c_recv_at: SystemTime,
200        api_response_at: SystemTime,
201    ) {
202        let Some(f3c_recv_us) = system_time_us(f3c_recv_at) else {
203            return;
204        };
205        let Some(api_response_us) = system_time_us(api_response_at) else {
206            return;
207        };
208
209        let TimeAdjustment {
210            s2c_time_diff_us,
211            net_delay_us,
212        } = *self.time_adjustment.lock();
213
214        let mut server_recv_us = server_recv_from_exchange_time_ms.unwrap_or(0) * 1000;
215        let mut server_send_us = server_send_to_client_time_ms.unwrap_or(0) * 1000;
216        if server_recv_us != 0 {
217            server_recv_us -= s2c_time_diff_us;
218        }
219        if server_send_us != 0 {
220            server_send_us -= s2c_time_diff_us;
221        }
222
223        // Ref: FutuOpenD/Src/NNDataCenter/Other/NNData_ProtoDelay.cpp:179-202.
224        // C++ corrects impossible clock skew (server send later than client
225        // receive) by preserving OpenD processing cost and shifting CR/CS by
226        // the measured one-way network delay.
227        let mut f3c_recv_us = f3c_recv_us;
228        let mut api_response_us = api_response_us;
229        if server_send_us > f3c_recv_us {
230            let open_d_cost_us = api_response_us.saturating_sub(f3c_recv_us);
231            f3c_recv_us = server_send_us + net_delay_us;
232            api_response_us = f3c_recv_us + open_d_cost_us;
233        }
234
235        let mut counts = self.qot_push_counts.lock();
236        let counter = counts.entry(qot_push_type).or_default();
237        counter.add(QOT_PUSH_STAGE_SR2SS, server_recv_us, server_send_us);
238        counter.add(QOT_PUSH_STAGE_SS2CR, server_send_us, f3c_recv_us);
239        counter.add(QOT_PUSH_STAGE_CR2CS, f3c_recv_us, api_response_us);
240        counter.add(QOT_PUSH_STAGE_SS2CS, server_send_us, api_response_us);
241        counter.add(QOT_PUSH_STAGE_SR2CS, server_recv_us, api_response_us);
242        counter.total_count = counter.total_count.saturating_add(1);
243    }
244
245    fn record_qot_push_count_if_sampled(
246        &self,
247        sample: QotPushDelaySample,
248    ) -> QotPushDelayRecordOutcome {
249        if !sample.count_delay {
250            return QotPushDelayRecordOutcome::SkippedByCountGate;
251        }
252        if !qot_push_should_sample_delay(sample.not_cal_delay) {
253            return QotPushDelayRecordOutcome::SkippedByNotCalDelay;
254        }
255
256        self.record_qot_push_count(
257            sample.qot_push_type,
258            sample.server_recv_from_exchange_time_ms,
259            sample.server_send_to_client_time_ms,
260            sample.f3c_recv_at,
261            sample.api_response_at,
262        );
263        QotPushDelayRecordOutcome::Recorded
264    }
265
266    fn snapshot_qot_push(
267        &self,
268        qot_push_stage: i32,
269        segment_list: &[i32],
270    ) -> Vec<QotPushStatisticsSnapshot> {
271        if qot_push_stage == 0 || segment_list.len() < 2 {
272            return Vec::new();
273        }
274
275        let counts = self.qot_push_counts.lock();
276        let mut items = Vec::new();
277        for qot_push_type in [
278            QOT_PUSH_TYPE_PRICE,
279            QOT_PUSH_TYPE_TICKER,
280            QOT_PUSH_TYPE_ORDER_BOOK,
281            QOT_PUSH_TYPE_BROKER,
282        ] {
283            let Some(counter) = counts.get(&qot_push_type) else {
284                continue;
285            };
286            let Some(stage) = counter.stages.get(&qot_push_stage) else {
287                continue;
288            };
289            if stage.total == 0 {
290                continue;
291            }
292
293            let mut cumulative_count = 0i32;
294            let total = stage.total as f32;
295            let item_list = segment_list
296                .windows(2)
297                .map(|window| {
298                    let begin = window[0];
299                    let end = window[1];
300                    let count = stage.range_count(begin, end) as i32;
301                    cumulative_count += count;
302                    DelayStatisticsItemSnapshot {
303                        begin,
304                        end,
305                        count,
306                        proportion: count as f32 / total * 100.0,
307                        cumulative_ratio: cumulative_count as f32 / total * 100.0,
308                    }
309                })
310                .collect();
311
312            items.push(QotPushStatisticsSnapshot {
313                qot_push_type,
314                item_list,
315                delay_avg_ms: stage.cost_avg_ms,
316                count: stage.total as i32,
317            });
318        }
319        items
320    }
321
322    fn record_place_order_req_detail(
323        &self,
324        ctx: &ApiRequestDelayContext,
325        backend_spans: &[BackendSpan],
326        api_end_at: SystemTime,
327    ) {
328        if ctx.proto_id != crate::proto_id::TRD_PLACE_ORDER {
329            return;
330        }
331        let Some(user_data) = ctx.place_order_user_data.lock().clone() else {
332            return;
333        };
334        let Some(api_begin_us) = system_time_us(ctx.begin_at) else {
335            return;
336        };
337        let Some(api_end_us) = system_time_us(api_end_at) else {
338            return;
339        };
340        if api_end_us < api_begin_us {
341            return;
342        }
343        let first_span = backend_spans.first();
344        let first_backend_begin_us = first_span.and_then(|span| system_time_us(span.begin_at));
345        let first_backend_end_us = first_span.and_then(|span| system_time_us(span.end_at));
346
347        self.place_order_req_details
348            .lock()
349            .push(PlaceOrderReqDetail {
350                order_id: user_data.order_id,
351                trd_env: user_data.trd_env,
352                _market: user_data.market,
353                api_begin_us,
354                api_end_us,
355                first_backend_begin_us,
356                first_backend_end_us,
357                net_delay_us: ctx.time_adjustment.net_delay_us,
358            });
359    }
360
361    fn set_place_order_user_data(&self, user_data: PlaceOrderUserData) {
362        let _ = CURRENT_API_REQUEST.try_with(|ctx| {
363            *ctx.place_order_user_data.lock() = Some(user_data);
364        });
365    }
366
367    fn record_place_order_update_push(
368        &self,
369        order_id: String,
370        trd_env: i32,
371        market: i32,
372        order_status: i32,
373        api_response_at: SystemTime,
374    ) {
375        let Some(api_response_us) = system_time_us(api_response_at) else {
376            return;
377        };
378        self.place_order_push_once.lock().push(PlaceOrderPushOnce {
379            order_id,
380            _trd_env: trd_env,
381            _market: market,
382            order_status,
383            api_response_us,
384        });
385    }
386
387    fn snapshot_place_order(&self) -> Vec<PlaceOrderStatisticsSnapshot> {
388        let mut earliest_push_by_order: HashMap<String, i64> = HashMap::new();
389        for push in self.place_order_push_once.lock().iter() {
390            if !place_order_update_status_counts(push.order_status) {
391                continue;
392            }
393            earliest_push_by_order
394                .entry(push.order_id.clone())
395                .and_modify(|existing| *existing = (*existing).min(push.api_response_us))
396                .or_insert(push.api_response_us);
397        }
398
399        self.place_order_req_details
400            .lock()
401            .iter()
402            .filter(|detail| detail.trd_env != 0)
403            .filter_map(|detail| {
404                let total_cost_us = detail.api_end_us.checked_sub(detail.api_begin_us)?;
405                let open_d_cost_us =
406                    match (detail.first_backend_begin_us, detail.first_backend_end_us) {
407                        (Some(begin), Some(end)) => begin
408                            .saturating_sub(detail.api_begin_us)
409                            .saturating_add(detail.api_end_us.saturating_sub(end)),
410                        _ => 0,
411                    };
412                let update_cost_us = earliest_push_by_order
413                    .get(&detail.order_id)
414                    .map(|push_us| push_us.saturating_sub(detail.api_end_us))
415                    .unwrap_or(0);
416
417                Some(PlaceOrderStatisticsSnapshot {
418                    order_id: detail.order_id.clone(),
419                    total_cost_ms: us_to_ms(total_cost_us),
420                    open_d_cost_ms: us_to_ms(open_d_cost_us),
421                    net_delay_ms: us_to_ms(detail.net_delay_us.max(0)),
422                    update_cost_ms: us_to_ms(update_cost_us),
423                })
424            })
425            .collect()
426    }
427}
428
429#[derive(Debug, Clone, Copy, Default)]
430struct TimeAdjustment {
431    s2c_time_diff_us: i64,
432    net_delay_us: i64,
433}
434
435#[derive(Debug, Clone)]
436struct PlaceOrderUserData {
437    order_id: String,
438    trd_env: i32,
439    market: i32,
440}
441
442#[derive(Debug, Clone)]
443struct PlaceOrderReqDetail {
444    order_id: String,
445    trd_env: i32,
446    _market: i32,
447    api_begin_us: i64,
448    api_end_us: i64,
449    first_backend_begin_us: Option<i64>,
450    first_backend_end_us: Option<i64>,
451    net_delay_us: i64,
452}
453
454#[derive(Debug, Clone)]
455struct PlaceOrderPushOnce {
456    order_id: String,
457    _trd_env: i32,
458    _market: i32,
459    order_status: i32,
460    api_response_us: i64,
461}
462
463#[derive(Debug)]
464struct ApiRequestDelayContext {
465    proto_id: u32,
466    begin: Instant,
467    begin_at: SystemTime,
468    backend_spans: Mutex<Vec<BackendSpan>>,
469    place_order_user_data: Mutex<Option<PlaceOrderUserData>>,
470    time_adjustment: TimeAdjustment,
471}
472
473impl ApiRequestDelayContext {
474    fn new(proto_id: u32) -> Self {
475        Self {
476            proto_id,
477            begin: Instant::now(),
478            begin_at: SystemTime::now(),
479            backend_spans: Mutex::new(Vec::new()),
480            place_order_user_data: Mutex::new(None),
481            time_adjustment: DELAY_STATS.time_adjustment(),
482        }
483    }
484
485    fn record_backend_span(
486        &self,
487        begin: Instant,
488        end: Instant,
489        begin_at: SystemTime,
490        end_at: SystemTime,
491    ) {
492        if end >= begin {
493            self.backend_spans.lock().push(BackendSpan {
494                begin,
495                end,
496                begin_at,
497                end_at,
498            });
499        }
500    }
501}
502
503#[derive(Debug, Clone, Copy)]
504struct BackendSpan {
505    begin: Instant,
506    end: Instant,
507    begin_at: SystemTime,
508    end_at: SystemTime,
509}
510
511static DELAY_STATS: LazyLock<DelayStatisticsStore> = LazyLock::new(DelayStatisticsStore::default);
512
513tokio::task_local! {
514    static CURRENT_API_REQUEST: Arc<ApiRequestDelayContext>;
515}
516
517/// Run a public API handler inside the current request/reply delay context.
518///
519/// C++ starts recording at `APIServerCS_Conn.cpp:279`
520/// (`ReqReply_APIReqBegin`) and records successful responses in
521/// `APIServer_Inner_API.h` via `ReqReply_APIReqEnd(..., bSuc)`.
522pub async fn with_api_request<F, Fut>(
523    _conn_id: u64,
524    _serial_no: u32,
525    proto_id: u32,
526    future: F,
527) -> Option<Vec<u8>>
528where
529    F: FnOnce() -> Fut,
530    Fut: Future<Output = Option<Vec<u8>>>,
531{
532    let ctx = Arc::new(ApiRequestDelayContext::new(proto_id));
533    let response = CURRENT_API_REQUEST.scope(ctx.clone(), future()).await;
534
535    if response
536        .as_deref()
537        .is_some_and(response_ret_type_is_success)
538    {
539        DELAY_STATS.record_req_reply(&ctx);
540    }
541
542    response
543}
544
545/// Record a backend request span for the API request currently executing on
546/// this Tokio task.  Calls outside public API dispatch are intentionally no-op,
547/// matching C++'s explicit `ReqReply_RelatedAPISvrReq` linking model.
548pub async fn trace_backend_request<Fut, T>(_cmd_id: u16, future: Fut) -> T
549where
550    Fut: Future<Output = T>,
551{
552    let begin = Instant::now();
553    let begin_at = SystemTime::now();
554    let output = future.await;
555    let end_at = SystemTime::now();
556    let end = Instant::now();
557
558    let _ =
559        CURRENT_API_REQUEST.try_with(|ctx| ctx.record_backend_span(begin, end, begin_at, end_at));
560    output
561}
562
563pub fn snapshot_req_reply_statistics() -> Vec<ReqReplyStatisticsSnapshot> {
564    DELAY_STATS.snapshot_req_reply()
565}
566
567/// Decide whether a QOT push should contribute to delay statistics.
568///
569/// Ref: FutuOpenD/Src/APIServer/Business/Quote/QotRealTimeData.cpp:2202-2210
570/// and the sibling push paths for BrokerQueue / OrderBook / US LV2 / Crypto
571/// LV2: C++ samples delay unless `not_cal_delay` is present and non-zero.
572#[must_use]
573pub fn qot_push_should_sample_delay(not_cal_delay: Option<i32>) -> bool {
574    match not_cal_delay {
575        Some(flag) => flag == 0,
576        None => true,
577    }
578}
579
580/// Select the server recv time used for QOT push delay statistics.
581///
582/// Ref: FutuOpenD/Src/APIServer/Business/Quote/QotRealTimeData.cpp:855-860,
583/// 1518-1544, 1635-1661, and 1942-1972.  C++ starts from `0` and updates
584/// `stPushTime.nSvrRecvTime_ms` only when a candidate recv time is greater
585/// than the current value, so missing/zero timestamps contribute no sample.
586#[must_use]
587pub fn qot_push_max_server_recv_time_ms<I>(times: I) -> Option<i64>
588where
589    I: IntoIterator<Item = Option<i64>>,
590{
591    times
592        .into_iter()
593        .flatten()
594        .filter(|time_ms| *time_ms > 0)
595        .max()
596}
597
598pub fn record_qot_push_count(
599    qot_push_type: i32,
600    server_recv_from_exchange_time_ms: Option<i64>,
601    server_send_to_client_time_ms: Option<i64>,
602    f3c_recv_at: SystemTime,
603    api_response_at: SystemTime,
604) {
605    DELAY_STATS.record_qot_push_count(
606        qot_push_type,
607        server_recv_from_exchange_time_ms,
608        server_send_to_client_time_ms,
609        f3c_recv_at,
610        api_response_at,
611    );
612}
613
614pub fn record_qot_push_count_if_sampled(sample: QotPushDelaySample) -> QotPushDelayRecordOutcome {
615    DELAY_STATS.record_qot_push_count_if_sampled(sample)
616}
617
618/// Attach successful `Trd_PlaceOrder` user data to the current API request.
619/// Calls outside `with_api_request` are intentionally no-op.
620///
621/// Ref: `APIServer_Trd_PlaceOrder.cpp:864-873` serializes `trdEnv`,
622/// `trdMarket`, and backend `order.szOrderID` into `ReqReply_SetUserData`.
623pub fn record_place_order_request(order_id: &str, trd_env: i32, market: i32) {
624    let order_id = order_id.trim();
625    if order_id.is_empty() {
626        return;
627    }
628    DELAY_STATS.set_place_order_user_data(PlaceOrderUserData {
629        order_id: order_id.to_string(),
630        trd_env,
631        market,
632    });
633}
634
635/// Record one `Trd_UpdateOrder` push candidate for PlaceOrder update-cost
636/// statistics.
637///
638/// Ref: `APIServer_Trd_UpdateOrder.cpp:36-38` records `Push_Once_Add` after
639/// sending the FTAPI push.  `api_response_at` should therefore be captured
640/// after the local push dispatch attempt.
641pub fn record_place_order_update_push(
642    order_id: &str,
643    trd_env: i32,
644    market: i32,
645    order_status: i32,
646    api_response_at: SystemTime,
647) {
648    let order_id = order_id.trim();
649    if order_id.is_empty() {
650        return;
651    }
652    DELAY_STATS.record_place_order_update_push(
653        order_id.to_string(),
654        trd_env,
655        market,
656        order_status,
657        api_response_at,
658    );
659}
660
661/// Update the server-to-client clock correction used by QOT push delay
662/// statistics.
663///
664/// Ref: `FutuOpenD/Src/NNProtoCenter/Other/NNBiz_SvrTime.cpp:41-78` calls
665/// `INNData_ProtoDelay::SetS2CTimeDiffAndNetDelay(nDiffTime_ms * 1000,
666/// nNetDelay_ms * 1000)`. `DelayCalibrationRuntime` supplies both values from
667/// the precise Platform server-time anchor and independent ICMP RTT/2. Before
668/// its first successful probe, startup may install a clock-only value with
669/// zero delay; probe failures preserve the last complete sample.
670pub fn set_time_adjustment(s2c_time_diff_us: i64, net_delay_us: i64) {
671    DELAY_STATS.set_time_adjustment(s2c_time_diff_us, net_delay_us);
672}
673
674pub fn snapshot_qot_push_statistics(
675    qot_push_stage: i32,
676    segment_list: &[i32],
677) -> Vec<QotPushStatisticsSnapshot> {
678    DELAY_STATS.snapshot_qot_push(qot_push_stage, segment_list)
679}
680
681pub fn snapshot_place_order_statistics() -> Vec<PlaceOrderStatisticsSnapshot> {
682    DELAY_STATS.snapshot_place_order()
683}
684
685fn backend_union_duration(spans: &[BackendSpan]) -> Duration {
686    if spans.is_empty() {
687        return Duration::ZERO;
688    }
689
690    let mut spans = spans.to_vec();
691    spans.sort_by_key(|span| span.begin);
692
693    let mut total = Duration::ZERO;
694    let mut cur_begin = spans[0].begin;
695    let mut cur_end = spans[0].end;
696
697    for span in spans.into_iter().skip(1) {
698        if span.begin <= cur_end {
699            if span.end > cur_end {
700                cur_end = span.end;
701            }
702        } else {
703            total += cur_end.duration_since(cur_begin);
704            cur_begin = span.begin;
705            cur_end = span.end;
706        }
707    }
708
709    total + cur_end.duration_since(cur_begin)
710}
711
712fn duration_ms(duration: Duration) -> f64 {
713    duration.as_secs_f64() * 1000.0
714}
715
716fn us_to_ms(us: i64) -> f32 {
717    us as f32 / 1000.0
718}
719
720fn system_time_us(time: SystemTime) -> Option<i64> {
721    let duration = time.duration_since(UNIX_EPOCH).ok()?;
722    i64::try_from(duration.as_micros()).ok()
723}
724
725fn place_order_update_status_counts(order_status: i32) -> bool {
726    // Ref: APIServer_GetDelayStatistics.cpp:323-324.
727    // NN_OrderStatus_Submitted / Filled_All / Cancelled_Part / Filled_Part
728    // map to FTAPI values 5 / 11 / 14 / 10.
729    matches!(order_status, 5 | 11 | 14 | 10)
730}
731
732#[cfg(test)]
733mod tests;