Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/base/bittorrent/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,14 @@ namespace BitTorrent
virtual void setUploadRateForSlowTorrents(int rateInKibiBytes) = 0;
virtual int slowTorrentsInactivityTimer() const = 0;
virtual void setSlowTorrentsInactivityTimer(int timeInSeconds) = 0;
virtual bool isSlowTorrentDetectionEnabled() const = 0;
virtual void setSlowTorrentDetectionEnabled(bool enabled) = 0;
virtual int slowTorrentDetectionDuration() const = 0;
virtual void setSlowTorrentDetectionDuration(int minutes) = 0;
virtual int slowTorrentMinimumProgress() const = 0;
virtual void setSlowTorrentMinimumProgress(int megabytes) = 0;
virtual QString slowTorrentExcludedTag() const = 0;
virtual void setSlowTorrentExcludedTag(const QString &tag) = 0;
virtual int outgoingPortsMin() const = 0;
virtual void setOutgoingPortsMin(int min) = 0;
virtual int outgoingPortsMax() const = 0;
Expand Down
195 changes: 195 additions & 0 deletions src/base/bittorrent/sessionimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ SessionImpl::SessionImpl(QObject *parent)
, m_downloadRateForSlowTorrents(BITTORRENT_SESSION_KEY(u"SlowTorrentsDownloadRate"_s), 2)
, m_uploadRateForSlowTorrents(BITTORRENT_SESSION_KEY(u"SlowTorrentsUploadRate"_s), 2)
, m_slowTorrentsInactivityTimer(BITTORRENT_SESSION_KEY(u"SlowTorrentsInactivityTimer"_s), 60)
, m_isSlowTorrentDetectionEnabled(BITTORRENT_SESSION_KEY(u"SlowTorrentDetectionEnabled"_s), false)
, m_slowTorrentDetectionDuration(BITTORRENT_SESSION_KEY(u"SlowTorrentDetectionDuration"_s), 10, lowerLimited(1))
, m_slowTorrentMinimumProgress(BITTORRENT_SESSION_KEY(u"SlowTorrentMinimumProgress"_s), 10, lowerLimited(1))
, m_slowTorrentExcludedTag(BITTORRENT_SESSION_KEY(u"SlowTorrentExcludedTag"_s))
, m_outgoingPortsMin(BITTORRENT_SESSION_KEY(u"OutgoingPortsMin"_s), 0)
, m_outgoingPortsMax(BITTORRENT_SESSION_KEY(u"OutgoingPortsMax"_s), 0)
, m_UPnPLeaseDuration(BITTORRENT_SESSION_KEY(u"UPnPLeaseDuration"_s), 0)
Expand Down Expand Up @@ -583,6 +587,7 @@ SessionImpl::SessionImpl(QObject *parent)
, m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)}
, m_seedingLimitTimer {new QTimer(this)}
, m_resumeDataTimer {new QTimer(this)}
, m_slowTorrentDetectionTimer {new QTimer(this)}
, m_ioThread {new QThread}
, m_asyncWorker {new QThreadPool(this)}
, m_recentErroredTorrentsTimer {new QTimer(this)}
Expand Down Expand Up @@ -619,6 +624,9 @@ SessionImpl::SessionImpl(QObject *parent)
processTorrentShareLimits(torrent);
});

m_slowTorrentDetectionTimer->setInterval(10s);
connect(m_slowTorrentDetectionTimer, &QTimer::timeout, this, &SessionImpl::processSlowTorrentDetection);

initializeNativeSession();
configureComponents();

Expand Down Expand Up @@ -4730,10 +4738,23 @@ void SessionImpl::setQueueingSystemEnabled(const bool enabled)
configureDeferred();

if (enabled)
{
m_torrentsQueueChanged = true;

// Start slow torrent detection timer if enabled
if (isSlowTorrentDetectionEnabled() && !m_slowTorrentDetectionTimer->isActive())
m_slowTorrentDetectionTimer->start();
}
else
{
removeTorrentsQueue();

// Stop slow torrent detection timer when queueing is disabled
if (m_slowTorrentDetectionTimer->isActive())
m_slowTorrentDetectionTimer->stop();
m_downloadProgressRecords.clear();
}

for (TorrentImpl *torrent : asConst(m_torrents))
torrent->handleQueueingModeChanged();
}
Expand Down Expand Up @@ -4840,6 +4861,71 @@ void SessionImpl::setSlowTorrentsInactivityTimer(const int timeInSeconds)
configureDeferred();
}

bool SessionImpl::isSlowTorrentDetectionEnabled() const
{
return m_isSlowTorrentDetectionEnabled;
}

void SessionImpl::setSlowTorrentDetectionEnabled(const bool enabled)
{
if (enabled != m_isSlowTorrentDetectionEnabled)
{
m_isSlowTorrentDetectionEnabled = enabled;

if (enabled && isQueueingSystemEnabled())
{
if (!m_slowTorrentDetectionTimer->isActive())
m_slowTorrentDetectionTimer->start();
}
else
{
if (m_slowTorrentDetectionTimer->isActive())
m_slowTorrentDetectionTimer->stop();
m_downloadProgressRecords.clear();
}
}
}

int SessionImpl::slowTorrentDetectionDuration() const
{
return m_slowTorrentDetectionDuration;
}

void SessionImpl::setSlowTorrentDetectionDuration(const int minutes)
{
if (minutes == m_slowTorrentDetectionDuration)
return;

m_slowTorrentDetectionDuration = minutes;
m_downloadProgressRecords.clear();
}

int SessionImpl::slowTorrentMinimumProgress() const
{
return m_slowTorrentMinimumProgress;
}

void SessionImpl::setSlowTorrentMinimumProgress(const int megabytes)
{
if (megabytes == m_slowTorrentMinimumProgress)
return;

m_slowTorrentMinimumProgress = megabytes;
}

QString SessionImpl::slowTorrentExcludedTag() const
{
return m_slowTorrentExcludedTag;
}

void SessionImpl::setSlowTorrentExcludedTag(const QString &tag)
{
if (tag == m_slowTorrentExcludedTag)
return;

m_slowTorrentExcludedTag = tag;
}

int SessionImpl::outgoingPortsMin() const
{
return m_outgoingPortsMin;
Expand Down Expand Up @@ -6615,3 +6701,112 @@ void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString

m_removingTorrents.erase(removingTorrentDataIter);
}

void SessionImpl::processSlowTorrentDetection()
{
// Only run if both queueing and slow torrent detection are enabled
if (!isQueueingSystemEnabled() || !isSlowTorrentDetectionEnabled())
{
m_downloadProgressRecords.clear();
return;
}

// Check if there are any queued torrents
bool hasQueuedTorrents = false;
for (TorrentImpl *torrent : asConst(m_torrents))
{
if (torrent->state() == TorrentState::QueuedDownloading)
{
hasQueuedTorrents = true;
break;
}
}

// If no queued torrents, clear records and return
if (!hasQueuedTorrents)
{
m_downloadProgressRecords.clear();
return;
}

// Calculate the number of samples needed for the monitoring window
// Using ceiling division: (duration_minutes * 60 + 9) / 10
// This ensures we have enough samples for the full monitoring window
// Example: 10 minutes = 600 seconds, 600/10 = 60 samples needed
const int monitoringSamplesCount = (m_slowTorrentDetectionDuration * 60 + 9) / 10;
const qint64 minimumProgressBytes = static_cast<qint64>(m_slowTorrentMinimumProgress) * 1024 * 1024;
const QString excludedTag = m_slowTorrentExcludedTag;

// Collect current download states for active downloading torrents
QHash<TorrentID, qint64> currentDownloadStates;
for (TorrentImpl *torrent : asConst(m_torrents))
{
const TorrentState state = torrent->state();

// Only monitor downloading, metaDL, and stalledDL states
if (state != TorrentState::Downloading
&& state != TorrentState::DownloadingMetadata
&& state != TorrentState::StalledDownloading)
{
continue;
}

// Skip if torrent has the excluded tag
if (!excludedTag.isEmpty())
{
const TagSet torrentTags = torrent->tags();
if (torrentTags.contains(Tag(excludedTag)))
continue;
}

currentDownloadStates.insert(torrent->id(), torrent->totalDownload());
}

// Remove records for torrents that are no longer being monitored
QMutableHashIterator<TorrentID, QList<qint64>> iter(m_downloadProgressRecords);
while (iter.hasNext())
{
iter.next();
if (!currentDownloadStates.contains(iter.key()))
iter.remove();
}

// Update progress records for each torrent
for (auto it = currentDownloadStates.cbegin(); it != currentDownloadStates.cend(); ++it)
{
const TorrentID &torrentID = it.key();
const qint64 downloadedBytes = it.value();

QList<qint64> &records = m_downloadProgressRecords[torrentID];
records.append(downloadedBytes);

// Keep only the last N samples
while (records.size() > monitoringSamplesCount)
records.removeFirst();
}

// Identify stagnant torrents that need to be moved to queue bottom
QList<TorrentID> stagnantTorrentIDs;
for (auto it = m_downloadProgressRecords.cbegin(); it != m_downloadProgressRecords.cend(); ++it)
{
const QList<qint64> &records = it.value();

// Only check torrents that have been monitored for the full window
if (records.size() == monitoringSamplesCount)
{
const qint64 progressDifference = records.last() - records.first();
if (progressDifference < minimumProgressBytes)
stagnantTorrentIDs.append(it.key());
}
}

// Move stagnant torrents to bottom of queue
if (!stagnantTorrentIDs.isEmpty())
{
bottomTorrentsQueuePos(stagnantTorrentIDs);

// Clear records for torrents that were moved to bottom
for (const TorrentID &torrentID : asConst(stagnantTorrentIDs))
m_downloadProgressRecords.remove(torrentID);
}
}
15 changes: 15 additions & 0 deletions src/base/bittorrent/sessionimpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ namespace BitTorrent
void setUploadRateForSlowTorrents(int rateInKibiBytes) override;
int slowTorrentsInactivityTimer() const override;
void setSlowTorrentsInactivityTimer(int timeInSeconds) override;
bool isSlowTorrentDetectionEnabled() const override;
void setSlowTorrentDetectionEnabled(bool enabled) override;
int slowTorrentDetectionDuration() const override;
void setSlowTorrentDetectionDuration(int minutes) override;
int slowTorrentMinimumProgress() const override;
void setSlowTorrentMinimumProgress(int megabytes) override;
QString slowTorrentExcludedTag() const override;
void setSlowTorrentExcludedTag(const QString &tag) override;
int outgoingPortsMin() const override;
void setOutgoingPortsMin(int min) override;
int outgoingPortsMax() const override;
Expand Down Expand Up @@ -521,6 +529,7 @@ namespace BitTorrent
void handleIPFilterParsed(int ruleCount);
void handleIPFilterError();
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
void processSlowTorrentDetection();

private:
struct ResumeSessionContext;
Expand Down Expand Up @@ -687,6 +696,10 @@ namespace BitTorrent
CachedSettingValue<int> m_downloadRateForSlowTorrents;
CachedSettingValue<int> m_uploadRateForSlowTorrents;
CachedSettingValue<int> m_slowTorrentsInactivityTimer;
CachedSettingValue<bool> m_isSlowTorrentDetectionEnabled;
CachedSettingValue<int> m_slowTorrentDetectionDuration;
CachedSettingValue<int> m_slowTorrentMinimumProgress;
CachedSettingValue<QString> m_slowTorrentExcludedTag;
CachedSettingValue<int> m_outgoingPortsMin;
CachedSettingValue<int> m_outgoingPortsMax;
CachedSettingValue<int> m_UPnPLeaseDuration;
Expand Down Expand Up @@ -815,6 +828,8 @@ namespace BitTorrent
bool m_refreshEnqueued = false;
QTimer *m_seedingLimitTimer = nullptr;
QTimer *m_resumeDataTimer = nullptr;
QTimer *m_slowTorrentDetectionTimer = nullptr;
QHash<TorrentID, QList<qint64>> m_downloadProgressRecords;
// IP filtering
QPointer<FilterParserThread> m_filterParser;
QPointer<BandwidthScheduler> m_bwScheduler;
Expand Down
23 changes: 23 additions & 0 deletions src/gui/optionsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,21 @@ void OptionsDialog::loadBittorrentTabOptions()
m_ui->spinUploadRateForSlowTorrents->setValue(session->uploadRateForSlowTorrents());
m_ui->spinSlowTorrentsInactivityTimer->setValue(session->slowTorrentsInactivityTimer());

m_ui->checkEnableSlowTorrentDetection->setChecked(session->isSlowTorrentDetectionEnabled());
m_ui->spinSlowTorrentDetectionDuration->setValue(session->slowTorrentDetectionDuration());
m_ui->spinSlowTorrentMinimumProgress->setValue(session->slowTorrentMinimumProgress());

// Populate tag combo box
m_ui->comboSlowTorrentExcludedTag->clear();
m_ui->comboSlowTorrentExcludedTag->addItem(tr("None"), QString());
const TagSet tags = session->tags();
for (const Tag &tag : tags)
m_ui->comboSlowTorrentExcludedTag->addItem(tag.toString(), tag.toString());

const QString excludedTag = session->slowTorrentExcludedTag();
const int tagIndex = m_ui->comboSlowTorrentExcludedTag->findData(excludedTag);
m_ui->comboSlowTorrentExcludedTag->setCurrentIndex(tagIndex >= 0 ? tagIndex : 0);

if (session->globalMaxRatio() >= 0.)
{
// Enable
Expand Down Expand Up @@ -1191,6 +1206,10 @@ void OptionsDialog::loadBittorrentTabOptions()
connect(m_ui->spinDownloadRateForSlowTorrents, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->spinUploadRateForSlowTorrents, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->spinSlowTorrentsInactivityTimer, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkEnableSlowTorrentDetection, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->spinSlowTorrentDetectionDuration, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->spinSlowTorrentMinimumProgress, qSpinBoxValueChanged, this, &ThisType::enableApplyButton);
connect(m_ui->comboSlowTorrentExcludedTag, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);

connect(m_ui->checkMaxRatio, &QAbstractButton::toggled, m_ui->spinMaxRatio, &QWidget::setEnabled);
connect(m_ui->checkMaxRatio, &QAbstractButton::toggled, this, &ThisType::toggleComboRatioLimitAct);
Expand Down Expand Up @@ -1233,6 +1252,10 @@ void OptionsDialog::saveBittorrentTabOptions() const
session->setDownloadRateForSlowTorrents(m_ui->spinDownloadRateForSlowTorrents->value());
session->setUploadRateForSlowTorrents(m_ui->spinUploadRateForSlowTorrents->value());
session->setSlowTorrentsInactivityTimer(m_ui->spinSlowTorrentsInactivityTimer->value());
session->setSlowTorrentDetectionEnabled(m_ui->checkEnableSlowTorrentDetection->isChecked());
session->setSlowTorrentDetectionDuration(m_ui->spinSlowTorrentDetectionDuration->value());
session->setSlowTorrentMinimumProgress(m_ui->spinSlowTorrentMinimumProgress->value());
session->setSlowTorrentExcludedTag(m_ui->comboSlowTorrentExcludedTag->currentData().toString());

session->setGlobalMaxRatio(getMaxRatio());
session->setGlobalMaxSeedingMinutes(getMaxSeedingMinutes());
Expand Down
Loading