Skip to content

Commit b5fb03b

Browse files
committed
Merge #784: Performance optimization: create a new torrent repository using DashMap
4030fd1 fix: torrent repository tests. DashMap is not ordered (Jose Celano) 1e76c17 chore: add dashmap cargo dep to cargp machete (Jose Celano) 00ee9db feat: [#565] new torrent repository implementation usind DashMap (Jose Celano) 78b46c4 chore(deps): add cargo dependency: dashmap (Jose Celano) Pull request description: Relates to: - #778 - #567 (comment) This PR adds a new torrent repository implementation where the outer collection for torrents uses a [DashMap](https://docs.rs/dashmap/latest/dashmap/). This is something @mickvandijke was working on this [PR](#645). There have been many changes. @da2ce7 has extracted a [package for the repositories](https://github.com/torrust/torrust-tracker/tree/develop/packages/torrent-repository). This PR adds a new repo using DashMap to the new package. However, it does not implement some of the extra features @mickvandijke added to the other [PR](#645). For example, it does not limit memory consumption. None of the other repos have that feature, so I suggest merging this PR and implementing that feature in the future for all repos. ### Why The current repository used in production is the one using a [SkipMap](https://docs.rs/crossbeam-skiplist/latest/crossbeam_skiplist/struct.SkipMap.html) data structure. DashMap was the first type we considered to allow adding new torrents in parallels. The implementation with DashMap has not been merged because it does not guarantee the order when you iterate over the torrents. The tracker API returns torrents ordered by InfoHash. To avoid breaking the API that PR would need to add a new data structure (kind of Index) to keep the torrent list ordered. This implementation helps us run performance tests with this option without spending much time fixing its limitations. It looks like the performance is similar to the SkiMap, so we don't need to use it in production now. We only need to keep it for benchmarking in the future if other versions are better. ### Benchmarking Running the Aquatic UDP load test, the DashMap looks slightly better. SkipMap: ```output Requests out: 396788.68/second Responses in: 357105.27/second - Connect responses: 176662.91 - Announce responses: 176863.44 - Scrape responses: 3578.91 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 105 - p99.9: 287 - p100: 351 ``` DashMap with initial capacity 0 (best result. On average is lower, similar to SkipMap): ```output Requests out: 410658.38/second Responses in: 365892.86/second - Connect responses: 181258.91 - Announce responses: 181005.95 - Scrape responses: 3628.00 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 104 - p99.9: 295 - p100: 363 ``` With Criterion: ![image](https://github.com/torrust/torrust-tracker/assets/58816/1e1fca2d-821d-4e2c-adf8-49e055758bd0) ![image](https://github.com/torrust/torrust-tracker/assets/58816/fcb9a32f-c4f1-4ffd-9797-4a74fc000336) ![image](https://github.com/torrust/torrust-tracker/assets/58816/efe2d788-31ac-4997-82f6-d022aa8f79a0) ![image](https://github.com/torrust/torrust-tracker/assets/58816/f64a4e5a-a363-48cd-87d9-78162cda11d4) ### Conclusion From my point of view, other [performance optimisations](#774) have more potential than this, considering this implementation is not finished. The changes needed to finish this implementation will probably decrease the performance obtained in this benchmarking. In the future, we can review this option if we change the [tracker API behaviour to getting all torrents](#775). ACKs for top commit: josecelano: ACK 4030fd1 Tree-SHA512: 1a6c56f3aecb34fc40c401597efe1663c3cc66903f2d27bf8f2bbc6b058080d487a89b705c224657aaa6059f1c2a8597583e636e30727f9293bb43f460441415
2 parents af52045 + 4030fd1 commit b5fb03b

File tree

10 files changed

+185
-11
lines changed

10 files changed

+185
-11
lines changed

Cargo.lock

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ clap = { version = "4", features = ["derive", "env"] }
4242
colored = "2"
4343
config = "0"
4444
crossbeam-skiplist = "0.1"
45+
dashmap = "5.5.3"
4546
derive_more = "0"
4647
fern = "0"
4748
futures = "0"
@@ -77,7 +78,7 @@ url = "2"
7778
uuid = { version = "1", features = ["v4"] }
7879

7980
[package.metadata.cargo-machete]
80-
ignored = ["serde_bytes", "crossbeam-skiplist"]
81+
ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap"]
8182

8283
[dev-dependencies]
8384
local-ip-address = "0"

cSpell.json

+1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
"Weidendorfer",
164164
"Werror",
165165
"whitespaces",
166+
"Xacrimon",
166167
"XBTT",
167168
"Xdebug",
168169
"Xeon",

packages/torrent-repository/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ version.workspace = true
1717

1818
[dependencies]
1919
crossbeam-skiplist = "0.1"
20+
dashmap = "5.5.3"
2021
futures = "0.3.29"
2122
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] }
2223
torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" }

packages/torrent-repository/benches/repository_benchmark.rs

+21-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ mod helpers;
44

55
use criterion::{criterion_group, criterion_main, Criterion};
66
use torrust_tracker_torrent_repository::{
7-
TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd,
8-
TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd,
7+
TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio,
8+
TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd,
99
};
1010

1111
use crate::helpers::{asyn, sync};
@@ -49,6 +49,10 @@ fn add_one_torrent(c: &mut Criterion) {
4949
b.iter_custom(sync::add_one_torrent::<TorrentsSkipMapMutexStd, _>);
5050
});
5151

52+
group.bench_function("DashMapMutexStd", |b| {
53+
b.iter_custom(sync::add_one_torrent::<TorrentsDashMapMutexStd, _>);
54+
});
55+
5256
group.finish();
5357
}
5458

@@ -98,6 +102,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) {
98102
.iter_custom(|iters| sync::add_multiple_torrents_in_parallel::<TorrentsSkipMapMutexStd, _>(&rt, iters, None));
99103
});
100104

105+
group.bench_function("DashMapMutexStd", |b| {
106+
b.to_async(&rt)
107+
.iter_custom(|iters| sync::add_multiple_torrents_in_parallel::<TorrentsDashMapMutexStd, _>(&rt, iters, None));
108+
});
109+
101110
group.finish();
102111
}
103112

@@ -147,6 +156,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) {
147156
.iter_custom(|iters| sync::update_one_torrent_in_parallel::<TorrentsSkipMapMutexStd, _>(&rt, iters, None));
148157
});
149158

159+
group.bench_function("DashMapMutexStd", |b| {
160+
b.to_async(&rt)
161+
.iter_custom(|iters| sync::update_one_torrent_in_parallel::<TorrentsDashMapMutexStd, _>(&rt, iters, None));
162+
});
163+
150164
group.finish();
151165
}
152166

@@ -197,6 +211,11 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) {
197211
.iter_custom(|iters| sync::update_multiple_torrents_in_parallel::<TorrentsSkipMapMutexStd, _>(&rt, iters, None));
198212
});
199213

214+
group.bench_function("DashMapMutexStd", |b| {
215+
b.to_async(&rt)
216+
.iter_custom(|iters| sync::update_multiple_torrents_in_parallel::<TorrentsDashMapMutexStd, _>(&rt, iters, None));
217+
});
218+
200219
group.finish();
201220
}
202221

packages/torrent-repository/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::sync::Arc;
22

3+
use repository::dash_map_mutex_std::XacrimonDashMap;
34
use repository::rw_lock_std::RwLockStd;
45
use repository::rw_lock_tokio::RwLockTokio;
56
use repository::skip_map_mutex_std::CrossbeamSkipList;
@@ -20,6 +21,7 @@ pub type TorrentsRwLockTokioMutexStd = RwLockTokio<EntryMutexStd>;
2021
pub type TorrentsRwLockTokioMutexTokio = RwLockTokio<EntryMutexTokio>;
2122

2223
pub type TorrentsSkipMapMutexStd = CrossbeamSkipList<EntryMutexStd>;
24+
pub type TorrentsDashMapMutexStd = XacrimonDashMap<EntryMutexStd>;
2325

2426
/// This code needs to be copied into each crate.
2527
/// Working version, for production.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::collections::BTreeMap;
2+
use std::sync::Arc;
3+
4+
use dashmap::DashMap;
5+
use torrust_tracker_configuration::TrackerPolicy;
6+
use torrust_tracker_primitives::info_hash::InfoHash;
7+
use torrust_tracker_primitives::pagination::Pagination;
8+
use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
9+
use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics;
10+
use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents};
11+
12+
use super::Repository;
13+
use crate::entry::{Entry, EntrySync};
14+
use crate::{EntryMutexStd, EntrySingle};
15+
16+
#[derive(Default, Debug)]
17+
pub struct XacrimonDashMap<T> {
18+
pub torrents: DashMap<InfoHash, T>,
19+
}
20+
21+
impl Repository<EntryMutexStd> for XacrimonDashMap<EntryMutexStd>
22+
where
23+
EntryMutexStd: EntrySync,
24+
EntrySingle: Entry,
25+
{
26+
fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) {
27+
if let Some(entry) = self.torrents.get(info_hash) {
28+
entry.insert_or_update_peer_and_get_stats(peer)
29+
} else {
30+
let _unused = self.torrents.insert(*info_hash, Arc::default());
31+
32+
match self.torrents.get(info_hash) {
33+
Some(entry) => entry.insert_or_update_peer_and_get_stats(peer),
34+
None => (false, SwarmMetadata::zeroed()),
35+
}
36+
}
37+
}
38+
39+
fn get(&self, key: &InfoHash) -> Option<EntryMutexStd> {
40+
let maybe_entry = self.torrents.get(key);
41+
maybe_entry.map(|entry| entry.clone())
42+
}
43+
44+
fn get_metrics(&self) -> TorrentsMetrics {
45+
let mut metrics = TorrentsMetrics::default();
46+
47+
for entry in &self.torrents {
48+
let stats = entry.value().lock().expect("it should get a lock").get_stats();
49+
metrics.complete += u64::from(stats.complete);
50+
metrics.downloaded += u64::from(stats.downloaded);
51+
metrics.incomplete += u64::from(stats.incomplete);
52+
metrics.torrents += 1;
53+
}
54+
55+
metrics
56+
}
57+
58+
fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexStd)> {
59+
match pagination {
60+
Some(pagination) => self
61+
.torrents
62+
.iter()
63+
.skip(pagination.offset as usize)
64+
.take(pagination.limit as usize)
65+
.map(|entry| (*entry.key(), entry.value().clone()))
66+
.collect(),
67+
None => self
68+
.torrents
69+
.iter()
70+
.map(|entry| (*entry.key(), entry.value().clone()))
71+
.collect(),
72+
}
73+
}
74+
75+
fn import_persistent(&self, persistent_torrents: &PersistentTorrents) {
76+
for (info_hash, completed) in persistent_torrents {
77+
if self.torrents.contains_key(info_hash) {
78+
continue;
79+
}
80+
81+
let entry = EntryMutexStd::new(
82+
EntrySingle {
83+
peers: BTreeMap::default(),
84+
downloaded: *completed,
85+
}
86+
.into(),
87+
);
88+
89+
self.torrents.insert(*info_hash, entry);
90+
}
91+
}
92+
93+
fn remove(&self, key: &InfoHash) -> Option<EntryMutexStd> {
94+
self.torrents.remove(key).map(|(_key, value)| value.clone())
95+
}
96+
97+
fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) {
98+
for entry in &self.torrents {
99+
entry.value().remove_inactive_peers(current_cutoff);
100+
}
101+
}
102+
103+
fn remove_peerless_torrents(&self, policy: &TrackerPolicy) {
104+
self.torrents.retain(|_, entry| entry.is_good(policy));
105+
}
106+
}

packages/torrent-repository/src/repository/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
55
use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics;
66
use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents};
77

8+
pub mod dash_map_mutex_std;
89
pub mod rw_lock_std;
910
pub mod rw_lock_std_mutex_std;
1011
pub mod rw_lock_std_mutex_tokio;

packages/torrent-repository/tests/common/repo.rs

+18-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics;
66
use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents};
77
use torrust_tracker_torrent_repository::repository::{Repository as _, RepositoryAsync as _};
88
use torrust_tracker_torrent_repository::{
9-
EntrySingle, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio,
10-
TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd,
9+
EntrySingle, TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio,
10+
TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd,
1111
};
1212

1313
#[derive(Debug)]
@@ -19,6 +19,7 @@ pub(crate) enum Repo {
1919
RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd),
2020
RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio),
2121
SkipMapMutexStd(TorrentsSkipMapMutexStd),
22+
DashMapMutexStd(TorrentsDashMapMutexStd),
2223
}
2324

2425
impl Repo {
@@ -31,6 +32,7 @@ impl Repo {
3132
Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()),
3233
Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()),
3334
Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()),
35+
Repo::DashMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()),
3436
}
3537
}
3638

@@ -43,6 +45,7 @@ impl Repo {
4345
Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await,
4446
Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await,
4547
Repo::SkipMapMutexStd(repo) => repo.get_metrics(),
48+
Repo::DashMapMutexStd(repo) => repo.get_metrics(),
4649
}
4750
}
4851

@@ -82,6 +85,11 @@ impl Repo {
8285
.iter()
8386
.map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone()))
8487
.collect(),
88+
Repo::DashMapMutexStd(repo) => repo
89+
.get_paginated(pagination)
90+
.iter()
91+
.map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone()))
92+
.collect(),
8593
}
8694
}
8795

@@ -94,6 +102,7 @@ impl Repo {
94102
Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await,
95103
Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await,
96104
Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents),
105+
Repo::DashMapMutexStd(repo) => repo.import_persistent(persistent_torrents),
97106
}
98107
}
99108

@@ -106,6 +115,7 @@ impl Repo {
106115
Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()),
107116
Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()),
108117
Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()),
118+
Repo::DashMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()),
109119
}
110120
}
111121

@@ -118,6 +128,7 @@ impl Repo {
118128
Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await,
119129
Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await,
120130
Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff),
131+
Repo::DashMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff),
121132
}
122133
}
123134

@@ -130,6 +141,7 @@ impl Repo {
130141
Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await,
131142
Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await,
132143
Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy),
144+
Repo::DashMapMutexStd(repo) => repo.remove_peerless_torrents(policy),
133145
}
134146
}
135147

@@ -146,6 +158,7 @@ impl Repo {
146158
Repo::RwLockTokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await,
147159
Repo::RwLockTokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await,
148160
Repo::SkipMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer),
161+
Repo::DashMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer),
149162
}
150163
}
151164

@@ -172,6 +185,9 @@ impl Repo {
172185
Repo::SkipMapMutexStd(repo) => {
173186
repo.torrents.insert(*info_hash, torrent.into());
174187
}
188+
Repo::DashMapMutexStd(repo) => {
189+
repo.torrents.insert(*info_hash, torrent.into());
190+
}
175191
};
176192
self.get(info_hash).await
177193
}

0 commit comments

Comments
 (0)