1use 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 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
517pub 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
545pub 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#[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#[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
618pub 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
635pub 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
661pub 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 matches!(order_status, 5 | 11 | 14 | 10)
730}
731
732#[cfg(test)]
733mod tests;