canic_core/ops/model/memory/
cycles.rs1pub use crate::model::memory::cycles::CycleTrackerView;
2
3use crate::{
4 cdk::{futures::spawn, utils::time::now_secs},
5 interface::ic::{
6 canister_cycle_balance,
7 timer::{Timer, TimerId},
8 },
9 log,
10 log::Topic,
11 model::memory::cycles::CycleTracker,
12 ops::{
13 config::ConfigOps,
14 model::memory::EnvOps,
15 model::{OPS_CYCLE_TRACK_INTERVAL, OPS_INIT_DELAY},
16 timer::TimerOps,
17 },
18 types::{Cycles, PageRequest},
19};
20use candid::CandidType;
21use serde::Serialize;
22use std::{cell::RefCell, time::Duration};
23
24thread_local! {
29 static TIMER: RefCell<Option<TimerId>> = const { RefCell::new(None) };
30}
31
32const TRACKER_INTERVAL_SECS: Duration = OPS_CYCLE_TRACK_INTERVAL;
38
39#[derive(CandidType, Serialize)]
44pub struct CycleTrackerPage {
45 pub entries: CycleTrackerView,
46 pub total: u64,
47}
48
49pub struct CycleTrackerOps;
54
55impl CycleTrackerOps {
56 pub fn start() {
59 TIMER.with_borrow_mut(|slot| {
60 if slot.is_some() {
61 return;
62 }
63
64 let init = TimerOps::set(OPS_INIT_DELAY, "cycles:init", async {
65 let _ = Self::track();
66
67 let interval =
68 TimerOps::set_interval(TRACKER_INTERVAL_SECS, "cycles:interval", || async {
69 let _ = Self::track();
70 let _ = Self::purge();
71 });
72
73 TIMER.with_borrow_mut(|slot| *slot = Some(interval));
74 });
75
76 *slot = Some(init);
77 });
78 }
79
80 pub fn stop() {
82 TIMER.with_borrow_mut(|slot| {
83 if let Some(id) = slot.take() {
84 Timer::clear(id);
85 }
86 });
87 }
88
89 #[must_use]
90 pub fn track() -> bool {
91 let ts = now_secs();
92 let cycles = canister_cycle_balance().to_u128();
93
94 if !EnvOps::is_root() {
96 Self::check_auto_topup();
97 }
98
99 CycleTracker::record(ts, cycles)
100 }
101
102 #[must_use]
104 pub fn purge() -> bool {
105 let now = now_secs();
106 CycleTracker::purge(now) > 0
107 }
108
109 fn check_auto_topup() {
110 use crate::ops::request::cycles_request;
111
112 if let Ok(canister_cfg) = ConfigOps::current_canister()
113 && let Some(topup) = canister_cfg.topup
114 {
115 let cycles = canister_cycle_balance();
116
117 if cycles < topup.threshold {
118 spawn(async move {
119 match cycles_request(topup.amount.to_u128()).await {
120 Ok(res) => log!(
121 Topic::Cycles,
122 Ok,
123 "💫 requested {}, topped up by {}, now {}",
124 topup.amount,
125 Cycles::from(res.cycles_transferred),
126 canister_cycle_balance()
127 ),
128 Err(e) => log!(Topic::Cycles, Error, "💫 failed to request cycles: {e}"),
129 }
130 });
131 }
132 }
133 }
134
135 #[must_use]
136 pub fn page(request: PageRequest) -> CycleTrackerPage {
137 let entries = CycleTracker::entries(request);
138 let total = CycleTracker::len();
139
140 CycleTrackerPage { entries, total }
141 }
142}