solana_runtime/
execute_cost_table.rs1use {
7 log::*, solana_program_runtime::compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT,
8 solana_sdk::pubkey::Pubkey, std::collections::HashMap,
9};
10
11const PRUNE_RATIO: usize = 75;
16const OCCURRENCES_WEIGHT: i64 = 100;
18
19const DEFAULT_CAPACITY: usize = 1024;
20
21#[derive(AbiExample, Debug)]
22pub struct ExecuteCostTable {
23 capacity: usize,
24 table: HashMap<Pubkey, u64>,
25 occurrences: HashMap<Pubkey, (usize, u128)>,
26}
27
28impl Default for ExecuteCostTable {
29 fn default() -> Self {
30 ExecuteCostTable::new(DEFAULT_CAPACITY)
31 }
32}
33
34impl ExecuteCostTable {
35 pub fn new(cap: usize) -> Self {
36 Self {
37 capacity: cap,
38 table: HashMap::with_capacity(cap),
39 occurrences: HashMap::with_capacity(cap),
40 }
41 }
42
43 pub fn get_count(&self) -> usize {
44 self.table.len()
45 }
46
47 pub fn get_default_compute_unit_limit(&self) -> u64 {
49 DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64
50 }
51
52 pub fn get_global_average_program_cost(&self) -> u64 {
54 if self.table.is_empty() {
55 self.get_default_compute_unit_limit()
56 } else {
57 self.table.values().sum::<u64>() / self.get_count() as u64
58 }
59 }
60
61 pub fn get_statistical_mode_program_cost(&self) -> u64 {
63 if self.occurrences.is_empty() {
64 self.get_default_compute_unit_limit()
65 } else {
66 let key = self
67 .occurrences
68 .iter()
69 .max_by_key(|&(_, count)| count)
70 .map(|(key, _)| key)
71 .expect("cannot find mode from cost table");
72
73 *self.table.get(key).unwrap()
74 }
75 }
76
77 pub fn get_cost(&self, key: &Pubkey) -> Option<&u64> {
82 self.table.get(key)
83 }
84
85 pub fn upsert(&mut self, key: &Pubkey, value: u64) {
88 let need_to_add = !self.table.contains_key(key);
89 let current_size = self.get_count();
90 if current_size >= self.capacity && need_to_add {
91 let prune_to_size = current_size
92 .checked_mul(PRUNE_RATIO)
93 .and_then(|v| v.checked_div(100))
94 .unwrap_or(self.capacity);
95 self.prune_to(&prune_to_size);
96 }
97
98 let program_cost = self.table.entry(*key).or_insert(value);
99 *program_cost = (*program_cost + value) / 2;
100
101 let (count, timestamp) = self
102 .occurrences
103 .entry(*key)
104 .or_insert((0, Self::micros_since_epoch()));
105 *count += 1;
106 *timestamp = Self::micros_since_epoch();
107 }
108
109 fn prune_to(&mut self, new_size: &usize) {
113 debug!(
114 "prune cost table, current size {}, new size {}",
115 self.get_count(),
116 new_size
117 );
118
119 if *new_size == self.get_count() {
120 return;
121 }
122
123 if *new_size == 0 {
124 self.table.clear();
125 self.occurrences.clear();
126 return;
127 }
128
129 let now = Self::micros_since_epoch();
130 let mut sorted_by_weighted_age: Vec<_> = self
131 .occurrences
132 .iter()
133 .map(|(key, (count, timestamp))| {
134 let age = now - timestamp;
135 let weighted_age = *count as i64 * OCCURRENCES_WEIGHT + -(age as i64);
136 (weighted_age, *key)
137 })
138 .collect();
139 sorted_by_weighted_age.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap());
140
141 for i in sorted_by_weighted_age.iter() {
142 self.table.remove(&i.1);
143 self.occurrences.remove(&i.1);
144 if *new_size == self.get_count() {
145 break;
146 }
147 }
148 }
149
150 fn micros_since_epoch() -> u128 {
151 std::time::SystemTime::now()
152 .duration_since(std::time::UNIX_EPOCH)
153 .unwrap()
154 .as_micros()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_execute_cost_table_prune_simple_table() {
164 solana_logger::setup();
165 let capacity: usize = 3;
166 let mut testee = ExecuteCostTable::new(capacity);
167
168 let key1 = Pubkey::new_unique();
169 let key2 = Pubkey::new_unique();
170 let key3 = Pubkey::new_unique();
171
172 testee.upsert(&key1, 1);
173 testee.upsert(&key2, 2);
174 testee.upsert(&key3, 3);
175
176 testee.prune_to(&(capacity - 1));
177
178 assert!(testee.get_cost(&key1).is_none());
180 assert!(testee.get_cost(&key2).is_some());
181 assert!(testee.get_cost(&key2).is_some());
182 }
183
184 #[test]
185 fn test_execute_cost_table_prune_weighted_table() {
186 solana_logger::setup();
187 let capacity: usize = 3;
188 let mut testee = ExecuteCostTable::new(capacity);
189
190 let key1 = Pubkey::new_unique();
191 let key2 = Pubkey::new_unique();
192 let key3 = Pubkey::new_unique();
193
194 for i in 0..1000 {
199 testee.upsert(&key1, i);
200 }
201 testee.upsert(&key2, 2);
202 testee.upsert(&key3, 3);
203
204 testee.prune_to(&(capacity - 1));
205
206 assert!(testee.get_cost(&key1).is_some());
209 assert!(testee.get_cost(&key2).is_none());
210 assert!(testee.get_cost(&key3).is_some());
211 }
212
213 #[test]
214 fn test_execute_cost_table_upsert_within_capacity() {
215 solana_logger::setup();
216 let mut testee = ExecuteCostTable::default();
217
218 let key1 = Pubkey::new_unique();
219 let key2 = Pubkey::new_unique();
220 let cost1: u64 = 100;
221 let cost2: u64 = 110;
222
223 assert!(testee.get_cost(&key1).is_none());
225
226 testee.upsert(&key1, cost1);
228 assert_eq!(1, testee.get_count());
229 assert_eq!(cost1, testee.get_global_average_program_cost());
230 assert_eq!(cost1, testee.get_statistical_mode_program_cost());
231 assert_eq!(&cost1, testee.get_cost(&key1).unwrap());
232
233 testee.upsert(&key2, cost2);
235 assert_eq!(2, testee.get_count());
236 assert_eq!(
237 (cost1 + cost2) / 2_u64,
238 testee.get_global_average_program_cost()
239 );
240 assert_eq!(cost2, testee.get_statistical_mode_program_cost());
241 assert_eq!(&cost1, testee.get_cost(&key1).unwrap());
242 assert_eq!(&cost2, testee.get_cost(&key2).unwrap());
243
244 testee.upsert(&key1, cost2);
246 assert_eq!(2, testee.get_count());
247 assert_eq!(
248 ((cost1 + cost2) / 2 + cost2) / 2_u64,
249 testee.get_global_average_program_cost()
250 );
251 assert_eq!(
252 (cost1 + cost2) / 2,
253 testee.get_statistical_mode_program_cost()
254 );
255 assert_eq!(&((cost1 + cost2) / 2), testee.get_cost(&key1).unwrap());
256 assert_eq!(&cost2, testee.get_cost(&key2).unwrap());
257 }
258
259 #[test]
260 fn test_execute_cost_table_upsert_exceeds_capacity() {
261 solana_logger::setup();
262 let capacity: usize = 2;
263 let mut testee = ExecuteCostTable::new(capacity);
264
265 let key1 = Pubkey::new_unique();
266 let key2 = Pubkey::new_unique();
267 let key3 = Pubkey::new_unique();
268 let key4 = Pubkey::new_unique();
269 let cost1: u64 = 100;
270 let cost2: u64 = 110;
271 let cost3: u64 = 120;
272 let cost4: u64 = 130;
273
274 testee.upsert(&key1, cost1);
276 assert_eq!(1, testee.get_count());
277 assert_eq!(&cost1, testee.get_cost(&key1).unwrap());
278
279 testee.upsert(&key2, cost2);
281 assert_eq!(2, testee.get_count());
282 assert_eq!(&cost1, testee.get_cost(&key1).unwrap());
283 assert_eq!(&cost2, testee.get_cost(&key2).unwrap());
284
285 testee.upsert(&key3, cost3);
287 assert_eq!(2, testee.get_count());
288 assert_eq!(
289 (cost2 + cost3) / 2_u64,
290 testee.get_global_average_program_cost()
291 );
292 assert_eq!(cost3, testee.get_statistical_mode_program_cost());
293 assert!(testee.get_cost(&key1).is_none());
294 assert_eq!(&cost2, testee.get_cost(&key2).unwrap());
295 assert_eq!(&cost3, testee.get_cost(&key3).unwrap());
296
297 testee.upsert(&key2, cost1);
300 testee.upsert(&key4, cost4);
301 assert_eq!(
302 ((cost1 + cost2) / 2 + cost4) / 2_u64,
303 testee.get_global_average_program_cost()
304 );
305 assert_eq!(
306 (cost1 + cost2) / 2,
307 testee.get_statistical_mode_program_cost()
308 );
309 assert_eq!(2, testee.get_count());
310 assert!(testee.get_cost(&key1).is_none());
311 assert_eq!(&((cost1 + cost2) / 2), testee.get_cost(&key2).unwrap());
312 assert!(testee.get_cost(&key3).is_none());
313 assert_eq!(&cost4, testee.get_cost(&key4).unwrap());
314 }
315}