Optimized the differential scan step by using threads to check files

This commit is contained in:
emeric
2025-07-19 17:10:15 +02:00
parent 97866d6143
commit 37c75f8677
7 changed files with 162 additions and 87 deletions
+9 -8
View File
@@ -23,6 +23,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp" #include "database/objects/ArtistInfo.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
@@ -367,17 +368,17 @@ namespace lms::db
}); });
} }
std::size_t Session::getTotalFilesCount() FileStats Session::getFileStats()
{ {
std::size_t res{}; FileStats stats{};
res += db::Track::getCount(*this); stats.trackCount = db::Track::getCount(*this);
res += db::Image::getCount(*this); stats.artistInfoCount = db::ArtistInfo::getCount(*this);
res += db::TrackLyrics::getExternalLyricsCount(*this); stats.imageCount = db::Image::getCount(*this);
res += db::PlayListFile::getCount(*this); stats.trackLyricsCount = db::TrackLyrics::getExternalLyricsCount(*this);
res += db::ArtistInfo::getCount(*this); stats.playListCount = db::PlayListFile::getCount(*this);
return res; return stats;
} }
void Session::retrieveEntriesToAnalyze(std::vector<std::string>& entryList) void Session::retrieveEntriesToAnalyze(std::vector<std::string>& entryList)
@@ -27,6 +27,7 @@
#include <vector> #include <vector>
#include "database/Transaction.hpp" #include "database/Transaction.hpp"
#include "database/Types.hpp"
namespace lms::db namespace lms::db
{ {
@@ -52,8 +53,8 @@ namespace lms::db
void retrieveEntriesToAnalyze(std::vector<std::string>& entryList); void retrieveEntriesToAnalyze(std::vector<std::string>& entryList);
void analyzeEntry(const std::string& entry); void analyzeEntry(const std::string& entry);
bool areAllTablesEmpty(); // need to acquire a read transaction bool areAllTablesEmpty(); // need to acquire a read transaction
std::size_t getTotalFilesCount(); // need to acquire a read transaction FileStats getFileStats(); // need to acquire a read transaction
void prepareTablesIfNeeded(); // need to run only once at startup void prepareTablesIfNeeded(); // need to run only once at startup
bool migrateSchemaIfNeeded(); // returns true if migration was performed bool migrateSchemaIfNeeded(); // returns true if migration was performed
@@ -102,6 +102,17 @@ namespace lms::db
} }
}; };
struct FileStats
{
std::size_t trackCount;
std::size_t imageCount;
std::size_t trackLyricsCount;
std::size_t playListCount;
std::size_t artistInfoCount;
std::size_t getTotalFileCount() const { return trackCount + imageCount + trackLyricsCount + playListCount + artistInfoCount; }
};
struct YearRange struct YearRange
{ {
int begin{}; int begin{};
@@ -166,17 +166,17 @@ namespace lms::scanner
LMS_LOG(DBUPDATER, INFO, "Using " << _jobScheduler->getThreadCount() << " thread(s) for jobs"); LMS_LOG(DBUPDATER, INFO, "Using " << _jobScheduler->getThreadCount() << " thread(s) for jobs");
_jobScheduler->setShouldAbortCallback([this]() { return _abortScan; }); _jobScheduler->setShouldAbortCallback([this]() { return _abortScan; });
std::size_t totalFilesCount{}; std::size_t totalFileCount{};
{ {
auto& session{ _db.getTLSSession() }; auto& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
totalFilesCount = session.getTotalFilesCount(); totalFileCount = session.getFileStats().getTotalFileCount();
} }
// Force optimize in case scanner aborted during a large import, but do this only if there are enough elements in the database // Force optimize in case scanner aborted during a large import, but do this only if there are enough elements in the database
// Otherwise, indexes may be not used and queries may be slower and slower while adding more and more elements in the db // Otherwise, indexes may be not used and queries may be slower and slower while adding more and more elements in the db
LMS_LOG(DBUPDATER, INFO, "Scanned file count = " << totalFilesCount); LMS_LOG(DBUPDATER, INFO, "Scanned file count = " << totalFileCount);
if (totalFilesCount >= 1'000) if (totalFileCount >= 1'000)
_db.getTLSSession().fullAnalyze(); _db.getTLSSession().fullAnalyze();
refreshTracingLoggerStats(); refreshTracingLoggerStats();
@@ -194,7 +194,7 @@ namespace lms::scanner
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
context.currentStepStats.totalElems = session.getTotalFilesCount(); context.currentStepStats.totalElems = session.getFileStats().getTotalFileCount();
} }
LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked..."); LMS_LOG(DBUPDATER, DEBUG, context.currentStepStats.totalElems << " files to be checked...");
@@ -19,12 +19,13 @@
#include "ScanStepScanFiles.hpp" #include "ScanStepScanFiles.hpp"
#include <deque>
#include "ScannerSettings.hpp" #include "ScannerSettings.hpp"
#include "core/IJob.hpp" #include "core/IJob.hpp"
#include "core/IJobScheduler.hpp" #include "core/IJobScheduler.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "core/Path.hpp"
#include "database/IDb.hpp" #include "database/IDb.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "scanners/FileToScan.hpp" #include "scanners/FileToScan.hpp"
@@ -93,28 +94,60 @@ namespace lms::scanner
class FileScanJob : public core::IJob class FileScanJob : public core::IJob
{ {
public: public:
FileScanJob(std::unique_ptr<IFileScanOperation> scanOperation) FileScanJob(const FileScanners& fileScanners, const MediaLibraryInfo& mediaLibrary, bool fullScan, std::span<const std::filesystem::directory_entry> files)
: _scanOperation{ std::move(scanOperation) } : _fileScanners{ fileScanners }
, _mediaLibrary{ mediaLibrary }
, _fullScan{ fullScan }
, _files{ std::cbegin(files), std::cend(files) }
{ {
} }
IFileScanOperation& getScanOperation() std::size_t getFileCount() const { return _files.size(); };
std::span<std::unique_ptr<IFileScanOperation>> getScanOperations()
{ {
return *_scanOperation; return _scanOperations;
} }
private: private:
core::LiteralString getName() const override core::LiteralString getName() const override
{ {
return _scanOperation->getName(); return "Scan Files";
} }
void run() override void run() override
{ {
_scanOperation->scan(); for (const auto& file : _files)
{
IFileScanner* scanner{ _fileScanners.select(file.path()) };
if (!scanner)
continue;
FileToScan fileToScan;
fileToScan.filePath = file.path();
fileToScan.mediaLibrary = _mediaLibrary;
fileToScan.lastWriteTime.setTime_t(Wt::WDateTime{ std::chrono::file_clock::to_sys(file.last_write_time()) }.toTime_t()); // sec resolution, as stored in the database
fileToScan.fileSize = file.file_size();
if (_fullScan || scanner->needsScan(fileToScan))
{
auto scanOperation{ scanner->createScanOperation(std::move(fileToScan)) };
{
LMS_SCOPED_TRACE_DETAILED("Scanner", scanOperation->getName());
scanOperation->scan();
}
_scanOperations.push_back(std::move(scanOperation));
}
}
} }
std::unique_ptr<IFileScanOperation> _scanOperation; const FileScanners& _fileScanners;
const MediaLibraryInfo& _mediaLibrary;
const bool _fullScan;
std::vector<std::filesystem::directory_entry> _files;
std::vector<std::unique_ptr<IFileScanOperation>> _scanOperations;
}; };
} // namespace } // namespace
@@ -128,96 +161,124 @@ namespace lms::scanner
{ {
for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries) for (const MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
process(context, mediaLibrary); process(context, mediaLibrary);
context.stats.totalFileCount = context.currentStepStats.processedElems;
} }
void ScanStepScanFiles::process(ScanContext& context, const MediaLibraryInfo& mediaLibrary) void ScanStepScanFiles::process(ScanContext& context, const MediaLibraryInfo& mediaLibrary)
{ {
const std::size_t scanQueueMaxScanRequestCount{ 50 * getJobScheduler().getThreadCount() }; constexpr std::size_t filesPerScanJob{ 10 };
constexpr std::size_t processFileResultsBatchSize{ 10 }; constexpr std::size_t scanQueueMaxSize{ 50 };
constexpr std::size_t processFileResultsBatchSize{ 1 };
constexpr float drainRatio{ 0.85 }; constexpr float drainRatio{ 0.85 };
std::deque<std::unique_ptr<IFileScanOperation>> operations;
auto processDoneJobs = [&](std::span<std::unique_ptr<core::IJob>> jobsDone) { auto processDoneJobs = [&](std::span<std::unique_ptr<core::IJob>> jobsDone) {
for (const auto& jobDone : jobsDone)
{
auto& fileScanJob{ static_cast<FileScanJob&>(*jobDone) };
for (std::unique_ptr<IFileScanOperation>& scanOperation : fileScanJob.getScanOperations())
operations.push_back(std::move(scanOperation));
context.currentStepStats.processedElems += fileScanJob.getFileCount();
}
if (!_abortScan) if (!_abortScan)
processFileScanResults(context, jobsDone); processFileScanOperations(context, operations, true /* force batch */);
_progressCallback(context.currentStepStats);
}; };
JobQueue queue{ getJobScheduler(), scanQueueMaxScanRequestCount, processDoneJobs, processFileResultsBatchSize, drainRatio }; {
JobQueue queue{ getJobScheduler(), scanQueueMaxSize, processDoneJobs, processFileResultsBatchSize, drainRatio };
std::vector<std::unique_ptr<core::IJob>> jobsDone; std::vector<std::filesystem::directory_entry> filesToScan;
std::vector<std::unique_ptr<IFileScanOperation>> scanOperations;
exploreFilesRecursive( exploreFilesRecursive(
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path, const std::filesystem::directory_entry* fileEntry) { mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path, const std::filesystem::directory_entry* fileEntry) {
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile"); LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
assert((ec && !fileEntry) || (!ec && fileEntry)); assert((ec && !fileEntry) || (!ec && fileEntry));
if (_abortScan) if (_abortScan)
return false; // stop iterating return false; // stop iterating
if (ec) if (ec)
{
addError<IOScanError>(context, path, ec);
context.stats.skips++;
}
else if (IFileScanner * scanner{ getFileScanners().select(path) })
{
FileToScan fileToScan;
fileToScan.filePath = path;
fileToScan.mediaLibrary = mediaLibrary;
fileToScan.lastWriteTime.setTime_t(Wt::WDateTime{ std::chrono::file_clock::to_sys(fileEntry->last_write_time()) }.toTime_t()); // sec resolution, as stored in the database
fileToScan.fileSize = fileEntry->file_size();
if (context.scanOptions.fullScan || scanner->needsScan(fileToScan))
{ {
auto scanOperation{ scanner->createScanOperation(std::move(fileToScan)) }; addError<IOScanError>(context, path, ec);
queue.push(std::make_unique<FileScanJob>(std::move(scanOperation))); context.stats.skips++;
}
else
{
filesToScan.push_back(*fileEntry);
if (filesToScan.size() >= filesPerScanJob)
{
queue.push(std::make_unique<FileScanJob>(getFileScanners(), mediaLibrary, context.scanOptions.fullScan, filesToScan));
filesToScan.clear();
}
} }
context.currentStepStats.processedElems++; return true;
_progressCallback(context.currentStepStats); },
} &excludeDirFileName);
return true; if (!filesToScan.empty())
}, queue.push(std::make_unique<FileScanJob>(getFileScanners(), mediaLibrary, context.scanOptions.fullScan, filesToScan));
&excludeDirFileName);
context.stats.totalFileCount = context.currentStepStats.totalElems; _progressCallback(context.currentStepStats);
}
// Process remaining objects
processFileScanOperations(context, operations, false /* force batch */);
} }
void ScanStepScanFiles::processFileScanResults(ScanContext& context, std::span<std::unique_ptr<core::IJob>> scanJobs) std::size_t ScanStepScanFiles::processFileScanOperations(ScanContext& context, std::deque<std::unique_ptr<IFileScanOperation>>& scanOperations, bool forceBatch)
{ {
std::size_t count{};
constexpr std::size_t writeBatchSize{ 10 };
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults"); LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ProcessScanResults");
db::Session& dbSession{ _db.getTLSSession() }; while ((forceBatch && scanOperations.size() >= writeBatchSize) || !scanOperations.empty())
auto transaction{ dbSession.createWriteTransaction() };
for (auto& scanJob : scanJobs)
{ {
IFileScanOperation& scanOperation{ static_cast<FileScanJob&>(*scanJob).getScanOperation() }; db::Session& dbSession{ _db.getTLSSession() };
auto transaction{ dbSession.createWriteTransaction() };
LMS_LOG(DBUPDATER, DEBUG, scanOperation.getName() << ": processing result for " << scanOperation.getFilePath()); for (std::size_t i{}; !scanOperations.empty() && i < writeBatchSize; ++i)
const IFileScanOperation::OperationResult res{ scanOperation.processResult() };
switch (res)
{ {
case IFileScanOperation::OperationResult::Added: processFileScanOperation(context, *scanOperations.front());
context.stats.additions++; scanOperations.pop_front();
break; count++;
case IFileScanOperation::OperationResult::Removed:
context.stats.deletions++;
break;
case IFileScanOperation::OperationResult::Skipped:
context.stats.failures++;
break;
case IFileScanOperation::OperationResult::Updated:
context.stats.updates++;
break;
} }
context.stats.scans++;
for (const auto& error : scanOperation.getErrors())
addError(context, error);
} }
return count;
}
void ScanStepScanFiles::processFileScanOperation(ScanContext& context, IFileScanOperation& scanOperation)
{
LMS_LOG(DBUPDATER, DEBUG, scanOperation.getName() << ": processing result for " << scanOperation.getFilePath());
const IFileScanOperation::OperationResult res{ scanOperation.processResult() };
switch (res)
{
case IFileScanOperation::OperationResult::Added:
context.stats.additions++;
break;
case IFileScanOperation::OperationResult::Removed:
context.stats.deletions++;
break;
case IFileScanOperation::OperationResult::Skipped:
context.stats.failures++;
break;
case IFileScanOperation::OperationResult::Updated:
context.stats.updates++;
break;
}
context.stats.scans++;
for (const auto& error : scanOperation.getErrors())
addError(context, error);
} }
} // namespace lms::scanner } // namespace lms::scanner
@@ -19,7 +19,7 @@
#pragma once #pragma once
#include <span> #include <deque>
#include "ScanStepBase.hpp" #include "ScanStepBase.hpp"
@@ -30,7 +30,7 @@ namespace lms::core
namespace lms::scanner namespace lms::scanner
{ {
class IFileScanner; class IFileScanOperation;
struct MediaLibraryInfo; struct MediaLibraryInfo;
class ScanStepScanFiles : public ScanStepBase class ScanStepScanFiles : public ScanStepBase
@@ -45,6 +45,7 @@ namespace lms::scanner
void process(ScanContext& context) override; void process(ScanContext& context) override;
void process(ScanContext& context, const MediaLibraryInfo& mediaLibrary); void process(ScanContext& context, const MediaLibraryInfo& mediaLibrary);
void processFileScanResults(ScanContext& context, std::span<std::unique_ptr<core::IJob>> scanJobs); std::size_t processFileScanOperations(ScanContext& context, std::deque<std::unique_ptr<IFileScanOperation>>& scanOperations, bool forceBatch);
void processFileScanOperation(ScanContext& context, IFileScanOperation& operation);
}; };
} // namespace lms::scanner } // namespace lms::scanner