Restore BitTorrent session asynchronously

Reduce the total startup time of the application and maintain sufficient responsiveness of the UI during startup due to the following:
1. Load resume data from disk asynchronously in separate thread;
2. Split handling of loaded resume data in chunks;
3. Reduce the number of emitting signals.

PR #16840.
This commit is contained in:
Vladimir Golovnev
2022-07-04 12:48:21 +03:00
committed by GitHub
parent ec1d2cba40
commit be7cfb78de
25 changed files with 927 additions and 553 deletions

View File

@@ -175,7 +175,7 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(Worker)
public:
Worker(const Path &dbPath, const QString &dbConnectionName);
Worker(const Path &dbPath, const QString &dbConnectionName, QReadWriteLock &dbLock);
void openDatabase() const;
void closeDatabase() const;
@@ -187,11 +187,64 @@ namespace BitTorrent
private:
const Path m_path;
const QString m_connectionName;
QReadWriteLock &m_dbLock;
};
namespace
{
LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
{
LoadTorrentParams resumeData;
resumeData.restored = true;
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
if (!tagsData.isEmpty())
{
const QStringList tagList = tagsData.split(u',');
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
}
resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
resumeData.savePath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
}
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
? bencodedResumeData
: (bencodedResumeData.chopped(1) + bencodedMetadata.mid(1)));
lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec);
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
p = lt::read_resume_data(root, ec);
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
.toString().toStdString();
return resumeData;
}
}
}
BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject *parent)
: ResumeDataStorage {parent}
: ResumeDataStorage(dbPath, parent)
, m_ioThread {new QThread(this)}
{
const bool needCreateDB = !dbPath.exists();
@@ -212,7 +265,7 @@ BitTorrent::DBResumeDataStorage::DBResumeDataStorage(const Path &dbPath, QObject
updateDBFromVersion1();
}
m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs);
m_asyncWorker = new Worker(dbPath, u"ResumeDataStorageWorker"_qs, m_dbLock);
m_asyncWorker->moveToThread(m_ioThread);
connect(m_ioThread, &QThread::finished, m_asyncWorker, &QObject::deleteLater);
m_ioThread->start();
@@ -262,7 +315,7 @@ QVector<BitTorrent::TorrentID> BitTorrent::DBResumeDataStorage::registeredTorren
return registeredTorrents;
}
std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
BitTorrent::LoadResumeDataResult BitTorrent::DBResumeDataStorage::load(const TorrentID &id) const
{
const QString selectTorrentStatement = u"SELECT * FROM %1 WHERE %2 = %3;"_qs
.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_TORRENT_ID.name), DB_COLUMN_TORRENT_ID.placeholder);
@@ -283,56 +336,11 @@ std::optional<BitTorrent::LoadTorrentParams> BitTorrent::DBResumeDataStorage::lo
}
catch (const RuntimeError &err)
{
LogMsg(tr("Couldn't load resume data of torrent '%1'. Error: %2")
.arg(id.toString(), err.message()), Log::CRITICAL);
return std::nullopt;
return nonstd::make_unexpected(tr("Couldn't load resume data of torrent '%1'. Error: %2")
.arg(id.toString(), err.message()));
}
LoadTorrentParams resumeData;
resumeData.restored = true;
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
if (!tagsData.isEmpty())
{
const QStringList tagList = tagsData.split(u',');
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
}
resumeData.hasSeedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
resumeData.savePath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
if (!resumeData.useAutoTMM)
{
resumeData.downloadPath = Profile::instance()->fromPortablePath(
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
}
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray();
const QByteArray allData = ((bencodedMetadata.isEmpty() || bencodedResumeData.isEmpty())
? bencodedResumeData
: (bencodedResumeData.chopped(1) + bencodedMetadata.mid(1)));
lt::error_code ec;
const lt::bdecode_node root = lt::bdecode(allData, ec);
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
p = lt::read_resume_data(root, ec);
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
.toString().toStdString();
return resumeData;
return parseQueryResultRow(query);
}
void BitTorrent::DBResumeDataStorage::store(const TorrentID &id, const LoadTorrentParams &resumeData) const
@@ -359,6 +367,49 @@ void BitTorrent::DBResumeDataStorage::storeQueue(const QVector<TorrentID> &queue
});
}
void BitTorrent::DBResumeDataStorage::doLoadAll() const
{
const QString connectionName = u"ResumeDataStorageLoadAll"_qs;
{
auto db = QSqlDatabase::addDatabase(u"QSQLITE"_qs, connectionName);
db.setDatabaseName(path().data());
if (!db.open())
throw RuntimeError(db.lastError().text());
QSqlQuery query {db};
const auto selectTorrentIDStatement = u"SELECT %1 FROM %2 ORDER BY %3;"_qs
.arg(quoted(DB_COLUMN_TORRENT_ID.name), quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
const QReadLocker locker {&m_dbLock};
if (!query.exec(selectTorrentIDStatement))
throw RuntimeError(query.lastError().text());
QVector<TorrentID> registeredTorrents;
registeredTorrents.reserve(query.size());
while (query.next())
registeredTorrents.append(TorrentID::fromString(query.value(0).toString()));
emit const_cast<DBResumeDataStorage *>(this)->loadStarted(registeredTorrents);
const auto selectStatement = u"SELECT * FROM %1 ORDER BY %2;"_qs.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_QUEUE_POSITION.name));
if (!query.exec(selectStatement))
throw RuntimeError(query.lastError().text());
while (query.next())
{
const auto torrentID = TorrentID::fromString(query.value(DB_COLUMN_TORRENT_ID.name).toString());
onResumeDataLoaded(torrentID, parseQueryResultRow(query));
}
}
emit const_cast<DBResumeDataStorage *>(this)->loadFinished();
QSqlDatabase::removeDatabase(connectionName);
}
int BitTorrent::DBResumeDataStorage::currentDBVersion() const
{
const auto selectDBVersionStatement = u"SELECT %1 FROM %2 WHERE %3 = %4;"_qs
@@ -372,6 +423,8 @@ int BitTorrent::DBResumeDataStorage::currentDBVersion() const
query.bindValue(DB_COLUMN_NAME.placeholder, META_VERSION);
const QReadLocker locker {&m_dbLock};
if (!query.exec())
throw RuntimeError(query.lastError().text());
@@ -390,6 +443,8 @@ void BitTorrent::DBResumeDataStorage::createDB() const
{
auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
const QWriteLocker locker {&m_dbLock};
if (!db.transaction())
throw RuntimeError(db.lastError().text());
@@ -453,6 +508,8 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
{
auto db = QSqlDatabase::database(DB_CONNECTION_NAME);
const QWriteLocker locker {&m_dbLock};
if (!db.transaction())
throw RuntimeError(db.lastError().text());
@@ -485,9 +542,10 @@ void BitTorrent::DBResumeDataStorage::updateDBFromVersion1() const
}
}
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, const QString &dbConnectionName)
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, const QString &dbConnectionName, QReadWriteLock &dbLock)
: m_path {dbPath}
, m_connectionName {dbConnectionName}
, m_dbLock {dbLock}
{
}
@@ -612,6 +670,7 @@ void BitTorrent::DBResumeDataStorage::Worker::store(const TorrentID &id, const L
if (!bencodedMetadata.isEmpty())
query.bindValue(DB_COLUMN_METADATA.placeholder, bencodedMetadata);
const QWriteLocker locker {&m_dbLock};
if (!query.exec())
throw RuntimeError(query.lastError().text());
}
@@ -636,6 +695,8 @@ void BitTorrent::DBResumeDataStorage::Worker::remove(const TorrentID &id) const
throw RuntimeError(query.lastError().text());
query.bindValue(DB_COLUMN_TORRENT_ID.placeholder, id.toString());
const QWriteLocker locker {&m_dbLock};
if (!query.exec())
throw RuntimeError(query.lastError().text());
}
@@ -656,6 +717,8 @@ void BitTorrent::DBResumeDataStorage::Worker::storeQueue(const QVector<TorrentID
try
{
const QWriteLocker locker {&m_dbLock};
if (!db.transaction())
throw RuntimeError(db.lastError().text());