1use std::collections::HashMap;
9use std::time::{Duration, Instant};
10
11pub const WINDOW_1_HOUR: Duration = Duration::from_secs(3600);
13pub const WINDOW_24_HOURS: Duration = Duration::from_secs(24 * 3600);
14pub const WINDOW_7_DAYS: Duration = Duration::from_secs(7 * 24 * 3600);
15
16#[derive(Debug, Clone)]
18pub struct PopularityConfig {
19 pub max_tracked_content: usize,
21 pub hot_window: Duration,
23 pub trending_window: Duration,
25 pub min_requests_for_popular: u64,
27 pub prune_interval: Duration,
29}
30
31impl Default for PopularityConfig {
32 #[inline]
33 fn default() -> Self {
34 Self {
35 max_tracked_content: 10000,
36 hot_window: WINDOW_1_HOUR,
37 trending_window: WINDOW_24_HOURS,
38 min_requests_for_popular: 10,
39 prune_interval: Duration::from_secs(3600), }
41 }
42}
43
44#[derive(Debug, Clone)]
46#[allow(dead_code)]
47struct AccessRecord {
48 timestamp: Instant,
49 bytes_transferred: u64,
50 peer_count: u32,
51}
52
53#[derive(Debug, Clone)]
55pub struct ContentPopularity {
56 pub cid: String,
58 pub total_requests: u64,
60 pub total_bytes: u64,
62 pub unique_peers: u64,
64 pub first_seen: Instant,
66 pub last_access: Instant,
68 access_history: Vec<AccessRecord>,
70}
71
72impl ContentPopularity {
73 #[inline]
74 fn new(cid: String) -> Self {
75 let now = Instant::now();
76 Self {
77 cid,
78 total_requests: 0,
79 total_bytes: 0,
80 unique_peers: 0,
81 first_seen: now,
82 last_access: now,
83 access_history: Vec::new(),
84 }
85 }
86
87 fn record_access(&mut self, bytes: u64, is_new_peer: bool) {
89 self.total_requests += 1;
90 self.total_bytes += bytes;
91 if is_new_peer {
92 self.unique_peers += 1;
93 }
94 self.last_access = Instant::now();
95
96 self.access_history.push(AccessRecord {
97 timestamp: Instant::now(),
98 bytes_transferred: bytes,
99 peer_count: if is_new_peer { 1 } else { 0 },
100 });
101 }
102
103 #[inline]
105 fn requests_in_window(&self, window: Duration) -> u64 {
106 let cutoff = Instant::now() - window;
107 self.access_history
108 .iter()
109 .filter(|r| r.timestamp > cutoff)
110 .count() as u64
111 }
112
113 #[inline]
115 fn bytes_in_window(&self, window: Duration) -> u64 {
116 let cutoff = Instant::now() - window;
117 self.access_history
118 .iter()
119 .filter(|r| r.timestamp > cutoff)
120 .map(|r| r.bytes_transferred)
121 .sum()
122 }
123
124 #[inline]
126 fn prune_history(&mut self, max_age: Duration) {
127 let cutoff = Instant::now() - max_age;
128 self.access_history.retain(|r| r.timestamp > cutoff);
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct PopularityScore {
135 pub cid: String,
137 pub score: f64,
139 pub hourly_requests: u64,
141 pub daily_requests: u64,
143 pub daily_bytes: u64,
145 pub unique_peers: u64,
147 pub demand_level: DemandLevel,
149 pub price_multiplier: f64,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum DemandLevel {
156 Low,
157 Medium,
158 High,
159 VeryHigh,
160}
161
162impl DemandLevel {
163 #[inline]
165 #[must_use]
166 pub fn price_multiplier(&self) -> f64 {
167 match self {
168 DemandLevel::Low => 0.5,
169 DemandLevel::Medium => 1.0,
170 DemandLevel::High => 1.5,
171 DemandLevel::VeryHigh => 3.0,
172 }
173 }
174}
175
176pub struct PopularityTracker {
178 config: PopularityConfig,
179 content: HashMap<String, ContentPopularity>,
180 peer_seen: HashMap<String, std::collections::HashSet<String>>,
181 last_prune: Instant,
182}
183
184impl Default for PopularityTracker {
185 #[inline]
186 fn default() -> Self {
187 Self::new(PopularityConfig::default())
188 }
189}
190
191impl PopularityTracker {
192 #[inline]
194 #[must_use]
195 pub fn new(config: PopularityConfig) -> Self {
196 Self {
197 config,
198 content: HashMap::new(),
199 peer_seen: HashMap::new(),
200 last_prune: Instant::now(),
201 }
202 }
203
204 pub fn record_access(&mut self, cid: &str, bytes: u64, peer_id: &str) {
206 let is_new_peer = self
208 .peer_seen
209 .entry(cid.to_string())
210 .or_default()
211 .insert(peer_id.to_string());
212
213 let popularity = self
215 .content
216 .entry(cid.to_string())
217 .or_insert_with(|| ContentPopularity::new(cid.to_string()));
218
219 popularity.record_access(bytes, is_new_peer);
220
221 self.maybe_prune();
223 }
224
225 #[inline]
227 #[must_use]
228 pub fn get_popularity(&self, cid: &str) -> Option<&ContentPopularity> {
229 self.content.get(cid)
230 }
231
232 #[must_use]
234 #[inline]
235 pub fn calculate_score(&self, cid: &str) -> Option<PopularityScore> {
236 let popularity = self.content.get(cid)?;
237
238 let hourly_requests = popularity.requests_in_window(self.config.hot_window);
239 let daily_requests = popularity.requests_in_window(self.config.trending_window);
240 let daily_bytes = popularity.bytes_in_window(self.config.trending_window);
241
242 let recency_score = calculate_recency_score(popularity.last_access);
244 let volume_score = calculate_volume_score(daily_requests);
245 let diversity_score = calculate_diversity_score(popularity.unique_peers, daily_requests);
246
247 let score = (recency_score * 0.3 + volume_score * 0.5 + diversity_score * 0.2) * 100.0;
249 let score = score.clamp(0.0, 100.0);
250
251 let demand_level = classify_demand(daily_requests, self.config.min_requests_for_popular);
252 let price_multiplier = demand_level.price_multiplier();
253
254 Some(PopularityScore {
255 cid: cid.to_string(),
256 score,
257 hourly_requests,
258 daily_requests,
259 daily_bytes,
260 unique_peers: popularity.unique_peers,
261 demand_level,
262 price_multiplier,
263 })
264 }
265
266 #[must_use]
268 #[inline]
269 pub fn get_top_content(&self, n: usize) -> Vec<PopularityScore> {
270 let mut scores: Vec<PopularityScore> = self
271 .content
272 .keys()
273 .filter_map(|cid| self.calculate_score(cid))
274 .collect();
275
276 scores.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
277 scores.truncate(n);
278 scores
279 }
280
281 #[must_use]
283 #[inline]
284 pub fn get_hot_content(&self) -> Vec<PopularityScore> {
285 let min_hourly = self.config.min_requests_for_popular / 24;
286
287 self.content
288 .keys()
289 .filter_map(|cid| self.calculate_score(cid))
290 .filter(|s| s.hourly_requests >= min_hourly)
291 .collect()
292 }
293
294 #[must_use]
296 #[inline]
297 pub fn get_trending_content(&self) -> Vec<PopularityScore> {
298 let mut scores: Vec<PopularityScore> = self
299 .content
300 .keys()
301 .filter_map(|cid| {
302 let score = self.calculate_score(cid)?;
303
304 let hourly_rate = score.hourly_requests as f64;
306 let daily_avg_rate = score.daily_requests as f64 / 24.0;
307
308 if hourly_rate > daily_avg_rate * 1.5 {
309 Some(score)
310 } else {
311 None
312 }
313 })
314 .collect();
315
316 scores.sort_by(|a, b| b.hourly_requests.cmp(&a.hourly_requests));
317 scores
318 }
319
320 #[must_use]
322 #[inline]
323 pub fn get_stats(&self) -> PopularityStats {
324 let total_content = self.content.len();
325 let total_requests: u64 = self.content.values().map(|p| p.total_requests).sum();
326 let total_bytes: u64 = self.content.values().map(|p| p.total_bytes).sum();
327
328 PopularityStats {
329 tracked_content: total_content,
330 total_requests,
331 total_bytes_transferred: total_bytes,
332 }
333 }
334
335 fn maybe_prune(&mut self) {
337 if Instant::now().duration_since(self.last_prune) < self.config.prune_interval {
338 return;
339 }
340
341 let max_history = self.config.trending_window * 2;
343 for popularity in self.content.values_mut() {
344 popularity.prune_history(max_history);
345 }
346
347 if self.content.len() > self.config.max_tracked_content {
349 let mut by_score: Vec<(String, u64)> = self
350 .content
351 .iter()
352 .map(|(cid, p)| {
353 (
354 cid.clone(),
355 p.requests_in_window(self.config.trending_window),
356 )
357 })
358 .collect();
359
360 by_score.sort_by(|a, b| a.1.cmp(&b.1));
361
362 let to_remove = self.content.len() - self.config.max_tracked_content;
364 for (cid, _) in by_score.into_iter().take(to_remove) {
365 self.content.remove(&cid);
366 self.peer_seen.remove(&cid);
367 }
368 }
369
370 self.last_prune = Instant::now();
371 }
372}
373
374#[derive(Debug, Clone)]
376pub struct PopularityStats {
377 pub tracked_content: usize,
379 pub total_requests: u64,
381 pub total_bytes_transferred: u64,
383}
384
385fn calculate_recency_score(last_access: Instant) -> f64 {
388 let age = Instant::now().duration_since(last_access);
389 let hours = age.as_secs_f64() / 3600.0;
390
391 0.5_f64.powf(hours / 24.0)
393}
394
395fn calculate_volume_score(daily_requests: u64) -> f64 {
396 if daily_requests == 0 {
398 return 0.0;
399 }
400 let log_requests = (daily_requests as f64).ln();
401 (log_requests / 10.0).min(1.0) }
403
404fn calculate_diversity_score(unique_peers: u64, total_requests: u64) -> f64 {
405 if total_requests == 0 {
406 return 0.0;
407 }
408 let ratio = unique_peers as f64 / total_requests as f64;
409 ratio.min(1.0) }
411
412fn classify_demand(daily_requests: u64, min_popular: u64) -> DemandLevel {
413 if daily_requests < min_popular / 2 {
414 DemandLevel::Low
415 } else if daily_requests < min_popular {
416 DemandLevel::Medium
417 } else if daily_requests < min_popular * 5 {
418 DemandLevel::High
419 } else {
420 DemandLevel::VeryHigh
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
429 fn test_record_access() {
430 let mut tracker = PopularityTracker::default();
431
432 tracker.record_access("QmTest123", 1024, "peer1");
433 tracker.record_access("QmTest123", 2048, "peer2");
434 tracker.record_access("QmTest123", 1024, "peer1"); let popularity = tracker.get_popularity("QmTest123").unwrap();
437 assert_eq!(popularity.total_requests, 3);
438 assert_eq!(popularity.total_bytes, 1024 + 2048 + 1024);
439 assert_eq!(popularity.unique_peers, 2);
440 }
441
442 #[test]
443 fn test_calculate_score() {
444 let mut tracker = PopularityTracker::default();
445
446 for i in 0..20 {
447 tracker.record_access("QmPopular", 1024, &format!("peer{}", i));
448 }
449
450 let score = tracker.calculate_score("QmPopular").unwrap();
451 assert!(score.score > 0.0);
452 assert_eq!(score.daily_requests, 20);
453 assert_eq!(score.unique_peers, 20);
454 }
455
456 #[test]
457 fn test_get_top_content() {
458 let mut tracker = PopularityTracker::default();
459
460 for i in 0..10 {
462 tracker.record_access("QmLow", 1024, &format!("peer{}", i));
463 }
464 for i in 0..50 {
465 tracker.record_access("QmMedium", 1024, &format!("peer{}", i));
466 }
467 for i in 0..100 {
468 tracker.record_access("QmHigh", 1024, &format!("peer{}", i));
469 }
470
471 let top = tracker.get_top_content(3);
472 assert_eq!(top.len(), 3);
473 assert_eq!(top[0].cid, "QmHigh");
474 }
475
476 #[test]
477 fn test_demand_classification() {
478 assert_eq!(classify_demand(0, 10), DemandLevel::Low);
479 assert_eq!(classify_demand(3, 10), DemandLevel::Low);
480 assert_eq!(classify_demand(7, 10), DemandLevel::Medium);
481 assert_eq!(classify_demand(15, 10), DemandLevel::High);
482 assert_eq!(classify_demand(100, 10), DemandLevel::VeryHigh);
483 }
484}