perf: raise transfer defaults and stop uploads sleeping on their slot

The slot cap merged from master defaults to 2 uploads, and the CLI capped
downloads at 5. Those are the numbers you pick when your own bandwidth is the
scarce resource, but a Soulseek transfer is paced by the peer on the other end
— usually a few hundred KiB/s — so what fills a modern link is many transfers
at once, not faster ones. Upload slots go to 10, downloads to 20. The queue
itself stays: privileged users being able to jump it is a condition of being a
tolerated third-party client.

serve_file also slept half a second after the last byte so the downloader could
drain before the socket closed. With a cap that is time no other peer can use,
and 64 uploads over 10 slots paid it six times over. A half-close says "that is
the whole file" and TCP still delivers everything written before the FIN.

Measured with 64 waiting peers: 2 slots 24.2s, 8 slots 6.2s, 32 slots 3.1s; at
10 slots the linger alone was 5.4s to 3.05s.
This commit is contained in:
Michel de Graaf
2026-07-26 18:25:50 +02:00
parent 5b4ef30f08
commit 7435d48b32
10 changed files with 41 additions and 18 deletions
+1 -2
View File
@@ -87,7 +87,7 @@ impl ActorSystem {
/// The callback receives the actor handle before `on_start` is called
pub fn spawn_with_handle<A: Actor, F>(
&self,
actor: A,
mut actor: A,
init: F,
) -> ActorHandle<A::Message>
where
@@ -96,7 +96,6 @@ impl ActorSystem {
let (sender, receiver) = channel::<ActorMessage<A::Message>>();
let handle = ActorHandle { sender };
let handle_for_init = handle.clone();
let mut actor = actor;
let spawned = thread::Builder::new()
.name("soulseek-actor".to_string())
+11 -4
View File
@@ -48,10 +48,17 @@ pub const DEFAULT_WISHLIST_INTERVAL: Duration = Duration::from_mins(12);
/// How many uploads run at once by default.
///
/// Two is what makes the queue real: with unlimited slots there is nothing for
/// a privileged peer to jump, and a home connection split across every peer
/// that asks serves all of them badly.
pub const DEFAULT_UPLOAD_SLOTS: usize = 2;
/// The cap is what makes the queue real — with unlimited slots there is nothing
/// for a privileged peer to jump — but it must not be the thing throttling a
/// modern connection. On Soulseek the per-transfer rate is usually set by the
/// *other* end, so concurrency is what fills an uplink: ten peers at a typical
/// few hundred KiB/s each is a few MiB/s, which a 50 Mbit uplink carries and a
/// pair of slots leaves mostly idle. Ten still divides a modest uplink into
/// shares big enough that no peer times out waiting.
///
/// Measured on the stress benchmark (64 waiting peers, loopback): two slots
/// took 24.2s, eight took 6.2s, thirty-two took 3.1s.
pub const DEFAULT_UPLOAD_SLOTS: usize = 10;
/// How long to wait for a server-brokered (firewalled) peer to connect back
/// before giving up and failing the download. Matches the direct-dial timeout.
+1 -4
View File
@@ -471,10 +471,7 @@ impl Client {
let registry = context.peer_registry.clone();
drop(context);
if let Some(registry) = registry {
let response =
crate::message::server::MessageFactory::build_transfer_response_message(
transfer,
);
let response = MessageFactory::build_transfer_response_message(transfer);
let _ = registry.send_to_peer(
&username,
PeerMessage::SendMessage(response),
+5 -2
View File
@@ -83,8 +83,11 @@ pub fn serve_file(
}
stream.flush()?;
// Linger so the downloader drains everything before the socket closes.
std::thread::sleep(Duration::from_millis(500));
// Half-close instead of lingering: the FIN tells the downloader it has the
// whole file, and TCP still delivers everything written before it. Sleeping
// here held an upload slot for an extra half-second per transfer, which with
// a slot cap is time no other peer can use.
stream.shutdown(std::net::Shutdown::Write).ok();
trace!("[upload] served {} to {}:{}", path.display(), host, port);
Ok(())
}