diff --git a/Cargo.lock b/Cargo.lock
index 669f9bd..3c3f028 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -613,9 +613,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.0.73"
+version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
[[package]]
name = "cfg-if"
@@ -1424,9 +1424,9 @@ dependencies = [
[[package]]
name = "hyper"
-version = "0.14.20"
+version = "0.14.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
+checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064"
dependencies = [
"bytes",
"futures-channel",
@@ -1557,9 +1557,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.136"
+version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55edcf6c0bb319052dea84732cf99db461780fd5e8d3eb46ab6ff312ab31f197"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libm"
@@ -1853,9 +1853,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "oorandom"
@@ -1975,9 +1975,9 @@ dependencies = [
[[package]]
name = "pkg-config"
-version = "0.3.25"
+version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "plotters"
@@ -2271,9 +2271,9 @@ dependencies = [
[[package]]
name = "scoped-tls"
-version = "1.0.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
diff --git a/aquatic_udp/src/workers/statistics.rs b/aquatic_udp/src/workers/statistics.rs
deleted file mode 100644
index ef754be..0000000
--- a/aquatic_udp/src/workers/statistics.rs
+++ /dev/null
@@ -1,379 +0,0 @@
-use std::fmt::Display;
-use std::fs::File;
-use std::io::Write;
-use std::sync::atomic::{AtomicUsize, Ordering};
-use std::sync::Arc;
-use std::time::{Duration, Instant};
-
-use anyhow::Context;
-use aquatic_common::PanicSentinel;
-use crossbeam_channel::Receiver;
-use hdrhistogram::Histogram;
-use num_format::{Locale, ToFormattedString};
-use serde::Serialize;
-use time::format_description::well_known::Rfc2822;
-use time::OffsetDateTime;
-use tinytemplate::TinyTemplate;
-
-use crate::common::*;
-use crate::config::Config;
-
-const TEMPLATE_KEY: &str = "statistics";
-const TEMPLATE_CONTENTS: &str = include_str!("../../templates/statistics.html");
-const STYLESHEET_CONTENTS: &str = concat!(
- ""
-);
-
-#[derive(Clone, Copy, Debug, Serialize)]
-struct PeerHistogramStatistics {
- p0: u64,
- p10: u64,
- p20: u64,
- p30: u64,
- p40: u64,
- p50: u64,
- p60: u64,
- p70: u64,
- p80: u64,
- p90: u64,
- p95: u64,
- p99: u64,
- p100: u64,
-}
-
-impl PeerHistogramStatistics {
- fn new(h: &Histogram) -> Self {
- Self {
- p0: h.value_at_percentile(0.0),
- p10: h.value_at_percentile(10.0),
- p20: h.value_at_percentile(20.0),
- p30: h.value_at_percentile(30.0),
- p40: h.value_at_percentile(40.0),
- p50: h.value_at_percentile(50.0),
- p60: h.value_at_percentile(60.0),
- p70: h.value_at_percentile(70.0),
- p80: h.value_at_percentile(80.0),
- p90: h.value_at_percentile(90.0),
- p95: h.value_at_percentile(95.0),
- p99: h.value_at_percentile(99.0),
- p100: h.value_at_percentile(100.0),
- }
- }
-}
-
-impl Display for PeerHistogramStatistics {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "p0: {}, p10: {}, p20: {}, p30: {}, p40: {}, p50: {}, p60: {}, p70: {}, p80: {}, p90: {}, p95: {}, p99: {}, p100: {}", self.p0, self.p10, self.p20, self.p30, self.p40, self.p50, self.p60, self.p70, self.p80, self.p90, self.p95, self.p99, self.p100)
- }
-}
-
-#[derive(Clone, Copy, Debug)]
-struct CollectedStatistics {
- requests_per_second: f64,
- responses_per_second_connect: f64,
- responses_per_second_announce: f64,
- responses_per_second_scrape: f64,
- responses_per_second_error: f64,
- bytes_received_per_second: f64,
- bytes_sent_per_second: f64,
- num_torrents: usize,
- num_peers: usize,
- peer_histogram: PeerHistogramStatistics,
-}
-
-impl CollectedStatistics {
- fn from_shared(
- statistics: &Arc,
- peer_histogram: &Histogram,
- last: &mut Instant,
- ) -> Self {
- let requests_received = statistics.requests_received.fetch_and(0, Ordering::Relaxed) as f64;
- let responses_sent_connect = statistics
- .responses_sent_connect
- .fetch_and(0, Ordering::Relaxed) as f64;
- let responses_sent_announce = statistics
- .responses_sent_announce
- .fetch_and(0, Ordering::Relaxed) as f64;
- let responses_sent_scrape = statistics
- .responses_sent_scrape
- .fetch_and(0, Ordering::Relaxed) as f64;
- let responses_sent_error = statistics
- .responses_sent_error
- .fetch_and(0, Ordering::Relaxed) as f64;
- let bytes_received = statistics.bytes_received.fetch_and(0, Ordering::Relaxed) as f64;
- let bytes_sent = statistics.bytes_sent.fetch_and(0, Ordering::Relaxed) as f64;
- let num_torrents = Self::sum_atomic_usizes(&statistics.torrents);
- let num_peers = Self::sum_atomic_usizes(&statistics.peers);
-
- let peer_histogram = PeerHistogramStatistics::new(peer_histogram);
-
- let now = Instant::now();
-
- let elapsed = (now - *last).as_secs_f64();
-
- *last = now;
-
- Self {
- requests_per_second: requests_received / elapsed,
- responses_per_second_connect: responses_sent_connect / elapsed,
- responses_per_second_announce: responses_sent_announce / elapsed,
- responses_per_second_scrape: responses_sent_scrape / elapsed,
- responses_per_second_error: responses_sent_error / elapsed,
- bytes_received_per_second: bytes_received / elapsed,
- bytes_sent_per_second: bytes_sent / elapsed,
- num_torrents,
- num_peers,
- peer_histogram,
- }
- }
-
- fn sum_atomic_usizes(values: &[AtomicUsize]) -> usize {
- values.iter().map(|n| n.load(Ordering::Relaxed)).sum()
- }
-}
-
-impl Into for CollectedStatistics {
- fn into(self) -> FormattedStatistics {
- let rx_mbits = self.bytes_received_per_second * 8.0 / 1_000_000.0;
- let tx_mbits = self.bytes_sent_per_second * 8.0 / 1_000_000.0;
-
- let responses_per_second_total = self.responses_per_second_connect
- + self.responses_per_second_announce
- + self.responses_per_second_scrape
- + self.responses_per_second_error;
-
- FormattedStatistics {
- requests_per_second: (self.requests_per_second as usize)
- .to_formatted_string(&Locale::en),
- responses_per_second_total: (responses_per_second_total as usize)
- .to_formatted_string(&Locale::en),
- responses_per_second_connect: (self.responses_per_second_connect as usize)
- .to_formatted_string(&Locale::en),
- responses_per_second_announce: (self.responses_per_second_announce as usize)
- .to_formatted_string(&Locale::en),
- responses_per_second_scrape: (self.responses_per_second_scrape as usize)
- .to_formatted_string(&Locale::en),
- responses_per_second_error: (self.responses_per_second_error as usize)
- .to_formatted_string(&Locale::en),
- rx_mbits: format!("{:.2}", rx_mbits),
- tx_mbits: format!("{:.2}", tx_mbits),
- num_torrents: self.num_torrents.to_formatted_string(&Locale::en),
- num_peers: self.num_peers.to_formatted_string(&Locale::en),
- peer_histogram: self.peer_histogram,
- }
- }
-}
-
-#[derive(Clone, Debug, Serialize)]
-struct FormattedStatistics {
- requests_per_second: String,
- responses_per_second_total: String,
- responses_per_second_connect: String,
- responses_per_second_announce: String,
- responses_per_second_scrape: String,
- responses_per_second_error: String,
- rx_mbits: String,
- tx_mbits: String,
- num_torrents: String,
- num_peers: String,
- peer_histogram: PeerHistogramStatistics,
-}
-
-#[derive(Debug, Serialize)]
-struct TemplateData {
- stylesheet: String,
- ipv4_active: bool,
- ipv6_active: bool,
- extended_active: bool,
- ipv4: FormattedStatistics,
- ipv6: FormattedStatistics,
- last_updated: String,
- peer_update_interval: String,
-}
-
-struct PeerHistograms {
- pending: Vec>,
- last_complete: Histogram,
-}
-
-impl Default for PeerHistograms {
- fn default() -> Self {
- Self {
- pending: Vec::new(),
- last_complete: Histogram::new(3).expect("create peer histogram"),
- }
- }
-}
-
-impl PeerHistograms {
- fn update(&mut self, config: &Config, histogram: Histogram) {
- self.pending.push(histogram);
-
- if self.pending.len() == config.swarm_workers {
- self.last_complete = self.pending.drain(..).sum();
- }
- }
-
- fn current(&self) -> &Histogram {
- &self.last_complete
- }
-}
-
-pub fn run_statistics_worker(
- _sentinel: PanicSentinel,
- config: Config,
- state: State,
- statistics_receiver: Receiver,
-) {
- let tt = if config.statistics.write_html_to_file {
- let mut tt = TinyTemplate::new();
-
- if let Err(err) = tt.add_template(TEMPLATE_KEY, TEMPLATE_CONTENTS) {
- ::log::error!("Couldn't parse statistics html template: {:#}", err);
-
- None
- } else {
- Some(tt)
- }
- } else {
- None
- };
-
- let mut last_ipv4 = Instant::now();
- let mut last_ipv6 = Instant::now();
-
- let mut peer_histograms_ipv4 = PeerHistograms::default();
- let mut peer_histograms_ipv6 = PeerHistograms::default();
-
- loop {
- ::std::thread::sleep(Duration::from_secs(config.statistics.interval));
-
- for message in statistics_receiver.try_iter() {
- match message {
- StatisticsMessage::Ipv4PeerHistogram(h) => peer_histograms_ipv4.update(&config, h),
- StatisticsMessage::Ipv6PeerHistogram(h) => peer_histograms_ipv6.update(&config, h),
- }
- }
-
- let statistics_ipv4 = CollectedStatistics::from_shared(
- &state.statistics_ipv4,
- peer_histograms_ipv4.current(),
- &mut last_ipv4,
- )
- .into();
- let statistics_ipv6 = CollectedStatistics::from_shared(
- &state.statistics_ipv6,
- peer_histograms_ipv6.current(),
- &mut last_ipv6,
- )
- .into();
-
- if config.statistics.print_to_stdout {
- println!("General:");
- println!(" access list entries: {}", state.access_list.load().len());
-
- if config.network.ipv4_active() {
- println!("IPv4:");
- print_to_stdout(&config, &statistics_ipv4);
- }
- if config.network.ipv6_active() {
- println!("IPv6:");
- print_to_stdout(&config, &statistics_ipv6);
- }
-
- println!();
- }
-
- if let Some(tt) = tt.as_ref() {
- let template_data = TemplateData {
- stylesheet: STYLESHEET_CONTENTS.to_string(),
- ipv4_active: config.network.ipv4_active(),
- ipv6_active: config.network.ipv6_active(),
- extended_active: config.statistics.extended,
- ipv4: statistics_ipv4,
- ipv6: statistics_ipv6,
- last_updated: OffsetDateTime::now_utc()
- .format(&Rfc2822)
- .unwrap_or("(formatting error)".into()),
- peer_update_interval: format!("{}", config.cleaning.torrent_cleaning_interval),
- };
-
- if let Err(err) = save_html_to_file(&config, tt, &template_data) {
- ::log::error!("Couldn't save statistics to file: {:#}", err)
- }
- }
- }
-}
-
-fn print_to_stdout(config: &Config, statistics: &FormattedStatistics) {
- println!(
- " bandwidth: {:>7} Mbit/s in, {:7} Mbit/s out",
- statistics.rx_mbits, statistics.tx_mbits,
- );
- println!(" requests/second: {:>10}", statistics.requests_per_second);
- println!(" responses/second");
- println!(
- " total: {:>10}",
- statistics.responses_per_second_total
- );
- println!(
- " connect: {:>10}",
- statistics.responses_per_second_connect
- );
- println!(
- " announce: {:>10}",
- statistics.responses_per_second_announce
- );
- println!(
- " scrape: {:>10}",
- statistics.responses_per_second_scrape
- );
- println!(
- " error: {:>10}",
- statistics.responses_per_second_error
- );
- println!(" torrents: {:>10}", statistics.num_torrents);
- println!(
- " peers: {:>10} (updated every {}s)",
- statistics.num_peers, config.cleaning.torrent_cleaning_interval
- );
-
- if config.statistics.extended {
- println!(
- " peers per torrent (updated every {}s)",
- config.cleaning.torrent_cleaning_interval
- );
- println!(" min {:>10}", statistics.peer_histogram.p0);
- println!(" p10 {:>10}", statistics.peer_histogram.p10);
- println!(" p20 {:>10}", statistics.peer_histogram.p20);
- println!(" p30 {:>10}", statistics.peer_histogram.p30);
- println!(" p40 {:>10}", statistics.peer_histogram.p40);
- println!(" p50 {:>10}", statistics.peer_histogram.p50);
- println!(" p60 {:>10}", statistics.peer_histogram.p60);
- println!(" p70 {:>10}", statistics.peer_histogram.p70);
- println!(" p80 {:>10}", statistics.peer_histogram.p80);
- println!(" p90 {:>10}", statistics.peer_histogram.p90);
- println!(" p95 {:>10}", statistics.peer_histogram.p95);
- println!(" p99 {:>10}", statistics.peer_histogram.p99);
- println!(" max {:>10}", statistics.peer_histogram.p100);
- }
-}
-
-fn save_html_to_file(
- config: &Config,
- tt: &TinyTemplate,
- template_data: &TemplateData,
-) -> anyhow::Result<()> {
- let mut file = File::create(&config.statistics.html_file_path).with_context(|| {
- format!(
- "File path: {}",
- &config.statistics.html_file_path.to_string_lossy()
- )
- })?;
-
- write!(file, "{}", tt.render(TEMPLATE_KEY, template_data)?)?;
-
- Ok(())
-}
diff --git a/aquatic_udp/src/workers/statistics/collector.rs b/aquatic_udp/src/workers/statistics/collector.rs
new file mode 100644
index 0000000..2e442ea
--- /dev/null
+++ b/aquatic_udp/src/workers/statistics/collector.rs
@@ -0,0 +1,175 @@
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Arc;
+use std::time::Instant;
+
+use hdrhistogram::Histogram;
+use num_format::{Locale, ToFormattedString};
+use serde::Serialize;
+
+use crate::common::Statistics;
+use crate::config::Config;
+
+pub struct StatisticsCollector {
+ shared: Arc,
+ last_update: Instant,
+ pending_histograms: Vec>,
+ last_complete_histogram: PeerHistogramStatistics,
+}
+
+impl StatisticsCollector {
+ pub fn new(shared: Arc) -> Self {
+ Self {
+ shared,
+ last_update: Instant::now(),
+ pending_histograms: Vec::new(),
+ last_complete_histogram: Default::default(),
+ }
+ }
+
+ pub fn add_histogram(&mut self, config: &Config, histogram: Histogram) {
+ self.pending_histograms.push(histogram);
+
+ if self.pending_histograms.len() == config.swarm_workers {
+ self.last_complete_histogram =
+ PeerHistogramStatistics::new(self.pending_histograms.drain(..).sum());
+ }
+ }
+
+ pub fn collect_from_shared(&mut self) -> FormattedStatistics {
+ let requests_received = Self::fetch_and_reset(&self.shared.requests_received);
+ let responses_sent_connect = Self::fetch_and_reset(&self.shared.responses_sent_connect);
+ let responses_sent_announce = Self::fetch_and_reset(&self.shared.responses_sent_announce);
+ let responses_sent_scrape = Self::fetch_and_reset(&self.shared.responses_sent_scrape);
+ let responses_sent_error = Self::fetch_and_reset(&self.shared.responses_sent_error);
+ let bytes_received = Self::fetch_and_reset(&self.shared.bytes_received);
+ let bytes_sent = Self::fetch_and_reset(&self.shared.bytes_sent);
+
+ let num_torrents = Self::sum_atomic_usizes(&self.shared.torrents);
+ let num_peers = Self::sum_atomic_usizes(&self.shared.peers);
+
+ let now = Instant::now();
+
+ let elapsed = (now - self.last_update).as_secs_f64();
+
+ self.last_update = now;
+
+ let collected_statistics = CollectedStatistics {
+ requests_per_second: requests_received / elapsed,
+ responses_per_second_connect: responses_sent_connect / elapsed,
+ responses_per_second_announce: responses_sent_announce / elapsed,
+ responses_per_second_scrape: responses_sent_scrape / elapsed,
+ responses_per_second_error: responses_sent_error / elapsed,
+ bytes_received_per_second: bytes_received / elapsed,
+ bytes_sent_per_second: bytes_sent / elapsed,
+ num_torrents,
+ num_peers,
+ };
+
+ FormattedStatistics::new(collected_statistics, self.last_complete_histogram.clone())
+ }
+
+ fn sum_atomic_usizes(values: &[AtomicUsize]) -> usize {
+ values.iter().map(|n| n.load(Ordering::Relaxed)).sum()
+ }
+
+ fn fetch_and_reset(atomic: &AtomicUsize) -> f64 {
+ atomic.fetch_and(0, Ordering::Relaxed) as f64
+ }
+}
+
+#[derive(Clone, Debug)]
+struct CollectedStatistics {
+ requests_per_second: f64,
+ responses_per_second_connect: f64,
+ responses_per_second_announce: f64,
+ responses_per_second_scrape: f64,
+ responses_per_second_error: f64,
+ bytes_received_per_second: f64,
+ bytes_sent_per_second: f64,
+ num_torrents: usize,
+ num_peers: usize,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct FormattedStatistics {
+ pub requests_per_second: String,
+ pub responses_per_second_total: String,
+ pub responses_per_second_connect: String,
+ pub responses_per_second_announce: String,
+ pub responses_per_second_scrape: String,
+ pub responses_per_second_error: String,
+ pub rx_mbits: String,
+ pub tx_mbits: String,
+ pub num_torrents: String,
+ pub num_peers: String,
+ pub peer_histogram: PeerHistogramStatistics,
+}
+
+impl FormattedStatistics {
+ fn new(statistics: CollectedStatistics, peer_histogram: PeerHistogramStatistics) -> Self {
+ let rx_mbits = statistics.bytes_received_per_second * 8.0 / 1_000_000.0;
+ let tx_mbits = statistics.bytes_sent_per_second * 8.0 / 1_000_000.0;
+
+ let responses_per_second_total = statistics.responses_per_second_connect
+ + statistics.responses_per_second_announce
+ + statistics.responses_per_second_scrape
+ + statistics.responses_per_second_error;
+
+ FormattedStatistics {
+ requests_per_second: (statistics.requests_per_second as usize)
+ .to_formatted_string(&Locale::en),
+ responses_per_second_total: (responses_per_second_total as usize)
+ .to_formatted_string(&Locale::en),
+ responses_per_second_connect: (statistics.responses_per_second_connect as usize)
+ .to_formatted_string(&Locale::en),
+ responses_per_second_announce: (statistics.responses_per_second_announce as usize)
+ .to_formatted_string(&Locale::en),
+ responses_per_second_scrape: (statistics.responses_per_second_scrape as usize)
+ .to_formatted_string(&Locale::en),
+ responses_per_second_error: (statistics.responses_per_second_error as usize)
+ .to_formatted_string(&Locale::en),
+ rx_mbits: format!("{:.2}", rx_mbits),
+ tx_mbits: format!("{:.2}", tx_mbits),
+ num_torrents: statistics.num_torrents.to_formatted_string(&Locale::en),
+ num_peers: statistics.num_peers.to_formatted_string(&Locale::en),
+ peer_histogram,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Serialize, Default)]
+pub struct PeerHistogramStatistics {
+ pub p0: u64,
+ pub p10: u64,
+ pub p20: u64,
+ pub p30: u64,
+ pub p40: u64,
+ pub p50: u64,
+ pub p60: u64,
+ pub p70: u64,
+ pub p80: u64,
+ pub p90: u64,
+ pub p95: u64,
+ pub p99: u64,
+ pub p100: u64,
+}
+
+impl PeerHistogramStatistics {
+ fn new(h: Histogram) -> Self {
+ Self {
+ p0: h.value_at_percentile(0.0),
+ p10: h.value_at_percentile(10.0),
+ p20: h.value_at_percentile(20.0),
+ p30: h.value_at_percentile(30.0),
+ p40: h.value_at_percentile(40.0),
+ p50: h.value_at_percentile(50.0),
+ p60: h.value_at_percentile(60.0),
+ p70: h.value_at_percentile(70.0),
+ p80: h.value_at_percentile(80.0),
+ p90: h.value_at_percentile(90.0),
+ p95: h.value_at_percentile(95.0),
+ p99: h.value_at_percentile(99.0),
+ p100: h.value_at_percentile(100.0),
+ }
+ }
+}
diff --git a/aquatic_udp/src/workers/statistics/mod.rs b/aquatic_udp/src/workers/statistics/mod.rs
new file mode 100644
index 0000000..9a2b0df
--- /dev/null
+++ b/aquatic_udp/src/workers/statistics/mod.rs
@@ -0,0 +1,185 @@
+mod collector;
+
+use std::fs::File;
+use std::io::Write;
+use std::time::Duration;
+
+use anyhow::Context;
+use aquatic_common::PanicSentinel;
+use crossbeam_channel::Receiver;
+use serde::Serialize;
+use time::format_description::well_known::Rfc2822;
+use time::OffsetDateTime;
+use tinytemplate::TinyTemplate;
+
+use collector::{FormattedStatistics, StatisticsCollector};
+
+use crate::common::*;
+use crate::config::Config;
+
+const TEMPLATE_KEY: &str = "statistics";
+const TEMPLATE_CONTENTS: &str = include_str!("../../../templates/statistics.html");
+const STYLESHEET_CONTENTS: &str = concat!(
+ ""
+);
+
+#[derive(Debug, Serialize)]
+struct TemplateData {
+ stylesheet: String,
+ ipv4_active: bool,
+ ipv6_active: bool,
+ extended_active: bool,
+ ipv4: FormattedStatistics,
+ ipv6: FormattedStatistics,
+ last_updated: String,
+ peer_update_interval: String,
+}
+
+pub fn run_statistics_worker(
+ _sentinel: PanicSentinel,
+ config: Config,
+ shared_state: State,
+ statistics_receiver: Receiver,
+) {
+ let opt_tt = if config.statistics.write_html_to_file {
+ let mut tt = TinyTemplate::new();
+
+ if let Err(err) = tt.add_template(TEMPLATE_KEY, TEMPLATE_CONTENTS) {
+ ::log::error!("Couldn't parse statistics html template: {:#}", err);
+
+ None
+ } else {
+ Some(tt)
+ }
+ } else {
+ None
+ };
+
+ let mut ipv4_collector = StatisticsCollector::new(shared_state.statistics_ipv4);
+ let mut ipv6_collector = StatisticsCollector::new(shared_state.statistics_ipv6);
+
+ loop {
+ ::std::thread::sleep(Duration::from_secs(config.statistics.interval));
+
+ for message in statistics_receiver.try_iter() {
+ match message {
+ StatisticsMessage::Ipv4PeerHistogram(h) => ipv4_collector.add_histogram(&config, h),
+ StatisticsMessage::Ipv6PeerHistogram(h) => ipv6_collector.add_histogram(&config, h),
+ }
+ }
+
+ let statistics_ipv4 = ipv4_collector.collect_from_shared();
+ let statistics_ipv6 = ipv6_collector.collect_from_shared();
+
+ if config.statistics.print_to_stdout {
+ println!("General:");
+ println!(
+ " access list entries: {}",
+ shared_state.access_list.load().len()
+ );
+
+ if config.network.ipv4_active() {
+ println!("IPv4:");
+ print_to_stdout(&config, &statistics_ipv4);
+ }
+ if config.network.ipv6_active() {
+ println!("IPv6:");
+ print_to_stdout(&config, &statistics_ipv6);
+ }
+
+ println!();
+ }
+
+ if let Some(tt) = opt_tt.as_ref() {
+ let template_data = TemplateData {
+ stylesheet: STYLESHEET_CONTENTS.to_string(),
+ ipv4_active: config.network.ipv4_active(),
+ ipv6_active: config.network.ipv6_active(),
+ extended_active: config.statistics.extended,
+ ipv4: statistics_ipv4,
+ ipv6: statistics_ipv6,
+ last_updated: OffsetDateTime::now_utc()
+ .format(&Rfc2822)
+ .unwrap_or("(formatting error)".into()),
+ peer_update_interval: format!("{}", config.cleaning.torrent_cleaning_interval),
+ };
+
+ if let Err(err) = save_html_to_file(&config, tt, &template_data) {
+ ::log::error!("Couldn't save statistics to file: {:#}", err)
+ }
+ }
+ }
+}
+
+fn print_to_stdout(config: &Config, statistics: &FormattedStatistics) {
+ println!(
+ " bandwidth: {:>7} Mbit/s in, {:7} Mbit/s out",
+ statistics.rx_mbits, statistics.tx_mbits,
+ );
+ println!(" requests/second: {:>10}", statistics.requests_per_second);
+ println!(" responses/second");
+ println!(
+ " total: {:>10}",
+ statistics.responses_per_second_total
+ );
+ println!(
+ " connect: {:>10}",
+ statistics.responses_per_second_connect
+ );
+ println!(
+ " announce: {:>10}",
+ statistics.responses_per_second_announce
+ );
+ println!(
+ " scrape: {:>10}",
+ statistics.responses_per_second_scrape
+ );
+ println!(
+ " error: {:>10}",
+ statistics.responses_per_second_error
+ );
+ println!(" torrents: {:>10}", statistics.num_torrents);
+ println!(
+ " peers: {:>10} (updated every {}s)",
+ statistics.num_peers, config.cleaning.torrent_cleaning_interval
+ );
+
+ if config.statistics.extended {
+ println!(
+ " peers per torrent (updated every {}s)",
+ config.cleaning.torrent_cleaning_interval
+ );
+ println!(" min {:>10}", statistics.peer_histogram.p0);
+ println!(" p10 {:>10}", statistics.peer_histogram.p10);
+ println!(" p20 {:>10}", statistics.peer_histogram.p20);
+ println!(" p30 {:>10}", statistics.peer_histogram.p30);
+ println!(" p40 {:>10}", statistics.peer_histogram.p40);
+ println!(" p50 {:>10}", statistics.peer_histogram.p50);
+ println!(" p60 {:>10}", statistics.peer_histogram.p60);
+ println!(" p70 {:>10}", statistics.peer_histogram.p70);
+ println!(" p80 {:>10}", statistics.peer_histogram.p80);
+ println!(" p90 {:>10}", statistics.peer_histogram.p90);
+ println!(" p95 {:>10}", statistics.peer_histogram.p95);
+ println!(" p99 {:>10}", statistics.peer_histogram.p99);
+ println!(" max {:>10}", statistics.peer_histogram.p100);
+ }
+}
+
+fn save_html_to_file(
+ config: &Config,
+ tt: &TinyTemplate,
+ template_data: &TemplateData,
+) -> anyhow::Result<()> {
+ let mut file = File::create(&config.statistics.html_file_path).with_context(|| {
+ format!(
+ "File path: {}",
+ &config.statistics.html_file_path.to_string_lossy()
+ )
+ })?;
+
+ write!(file, "{}", tt.render(TEMPLATE_KEY, template_data)?)?;
+
+ Ok(())
+}