From 1862cd23c2242a4497c4e67f798a2d4dcbc9e537 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 18 Apr 2024 18:46:35 +0200 Subject: [PATCH] Changed db optimization strategy: now perform a full analyze (only after scan if enough changes has been made, and during application startup). Also perform a vacuum if needed during startup. Added an init screen for the web interface since it can take a while to complete --- approot/messages.xml | 1 + approot/messages_fr.xml | 1 + approot/messages_it.xml | 1 + approot/messages_zh.xml | 1 + src/libs/core/include/core/LiteralString.hpp | 7 + src/libs/database/impl/Db.cpp | 12 +- src/libs/database/impl/Session.cpp | 241 ++++++++++-------- .../database/include/database/Session.hpp | 13 +- src/libs/database/test/Common.cpp | 4 +- src/libs/services/scanner/CMakeLists.txt | 1 + src/libs/services/scanner/impl/IScanStep.hpp | 4 +- .../services/scanner/impl/ScanStepAnalyze.cpp | 53 ++++ .../services/scanner/impl/ScanStepAnalyze.hpp | 36 +++ .../impl/ScanStepCheckDuplicatedDbFiles.hpp | 2 +- .../impl/ScanStepComputeClusterStats.cpp | 1 + .../impl/ScanStepComputeClusterStats.hpp | 2 +- .../scanner/impl/ScanStepDiscoverFiles.hpp | 2 +- .../impl/ScanStepRemoveOrphanDbFiles.hpp | 2 +- .../scanner/impl/ScanStepScanFiles.cpp | 4 - .../scanner/impl/ScanStepScanFiles.hpp | 2 +- .../services/scanner/impl/ScannerService.cpp | 13 +- .../include/services/scanner/ScannerStats.hpp | 3 +- src/lms/CMakeLists.txt | 1 + src/lms/main.cpp | 31 ++- src/lms/ui/LmsInitApplication.cpp | 38 +++ src/lms/ui/LmsInitApplication.hpp | 33 +++ src/lms/ui/admin/ScannerController.cpp | 7 + 27 files changed, 382 insertions(+), 134 deletions(-) create mode 100644 src/libs/services/scanner/impl/ScanStepAnalyze.cpp create mode 100644 src/libs/services/scanner/impl/ScanStepAnalyze.hpp create mode 100644 src/lms/ui/LmsInitApplication.cpp create mode 100644 src/lms/ui/LmsInitApplication.hpp diff --git a/approot/messages.xml b/approot/messages.xml index e78a91e9..c494f326 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -105,6 +105,7 @@ Not scheduled Scheduled on {1} Scanning: step {1}/{2} +Analyzing database... {1}/{2} entries ({3}%)... Checking for duplicate files... {1} files Checking files... {1}% Computing stats... {1}% diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index b0627408..797355a1 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -105,6 +105,7 @@ Non planifié Planifié le {1} En cours de scan : étape {1}/{2} +Analyze de la base de données... {1}/{2} entrées ({3}%)... Vérification des fichiers dupliqués... {1} fichiers Vérification des fichiers... {1}% Calcul des statistiques... {1}% diff --git a/approot/messages_it.xml b/approot/messages_it.xml index aa675ec5..640b1091 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -105,6 +105,7 @@ Non pianificato Pianificato il {1} Scansione: passo {1}/{2} +Analisi del database... {1}/{2} voci ({3}%)... Controllo duplicati... {1} files Controllo file... {1}% Calcolo statistiche... {1}% diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 3a89f689..6a8ac377 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -106,6 +106,7 @@ 计划于 {1} 扫描中: 阶段 {1}/{2} + 检查文件中... {1}% 检索文件中: {1} 文件 diff --git a/src/libs/core/include/core/LiteralString.hpp b/src/libs/core/include/core/LiteralString.hpp index 7c0394bd..11b7f668 100644 --- a/src/libs/core/include/core/LiteralString.hpp +++ b/src/libs/core/include/core/LiteralString.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include @@ -41,6 +42,12 @@ namespace lms::core private: std::string_view _str; }; + + inline std::ostream& operator<<(std::ostream& os, const LiteralString& str) + { + os << str.str(); + return os; + } } namespace std diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index d756d1f5..23afed43 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -25,8 +25,9 @@ #include "database/Session.hpp" #include "database/User.hpp" #include "core/IConfig.hpp" -#include "core/Service.hpp" #include "core/ILogger.hpp" +#include "core/ITraceLogger.hpp" +#include "core/Service.hpp" namespace lms::db { @@ -49,7 +50,7 @@ namespace lms::db prepare(); } - ~Connection() + ~Connection() override { // make use of per-connection usage stats to optimize optimize(); @@ -66,16 +67,15 @@ namespace lms::db void prepare() { LMS_LOG(DB, DEBUG, "Setting per-connection settings..."); - executeSql("pragma journal_mode=WAL"); - executeSql("pragma synchronous=normal"); - executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries + executeSql("PRAGMA journal_mode=WAL"); + executeSql("PRAGMA synchronous=normal"); LMS_LOG(DB, DEBUG, "Setting per-connection settings done!"); } void optimize() { LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize..."); - executeSql("pragma optimize"); + executeSql("PRAGMA optimize"); LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete"); } diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 757bdcd3..ded20802 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -46,6 +46,7 @@ #include "EnumSetTraits.hpp" #include "PathTraits.hpp" #include "Migration.hpp" +#include "Utils.hpp" namespace lms::db { @@ -120,7 +121,7 @@ namespace lms::db return ReadTransaction{ _session }; } - void Session::prepareTables() + void Session::prepareTablesIfNeeded() { LMS_LOG(DB, INFO, "Preparing tables..."); @@ -140,127 +141,165 @@ namespace lms::db throw e; } } + } + void Session::migrateIfNeeded() + { Migration::doDbMigration(*this); - // Indexes - { - LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation"); - - auto transaction{ createWriteTransaction() }; - _session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); - - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); - _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); - - _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); - - _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); - - _session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); - - _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); - - _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); - - _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)"); - - _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)"); - _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)"); - } - - // Singletons + // TODO: move this elsewhere { auto uniqueTransaction{ createWriteTransaction() }; ScanSettings::init(*this); } } + void Session::createIndexesIfNeeded() + { + LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation"); + + auto transaction{ createWriteTransaction() }; + _session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); + + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)"); + + _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); + + _session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)"); + + _session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)"); + _session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)"); + + _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)"); + _session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)"); + + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)"); + + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)"); + + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)"); + _session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)"); + } + + void Session::vacuumIfNeeded() + { + long pageCount{}; + long freeListCount{}; + + { + auto transaction{ createReadTransaction() }; + pageCount = utils::fetchQuerySingleResult(_session.query("SELECT page_count FROM pragma_page_count")); + freeListCount = utils::fetchQuerySingleResult(_session.query("SELECT freelist_count FROM pragma_freelist_count")); + } + + LMS_LOG(DB, INFO, "page stats: page_count = " << pageCount << ", freelist_count = " << freeListCount); + if (freeListCount >= (pageCount / 10)) + { + LMS_SCOPED_TRACE_OVERVIEW("Database", "Vacuum"); + LMS_LOG(DB, INFO, "Performing vacuum... This may take a while..."); + + // We manually take a lock here since vacuum cannot be inside a transaction + { + std::unique_lock lock{ _db.getMutex() }; + _db.executeSql("VACUUM"); + } + + LMS_LOG(DB, INFO, "Vacuum complete!"); + } + } + void Session::refreshTracingLoggerStats() { auto* traceLogger{ core::Service::get() }; if (!traceLogger) return; - auto& dbSession{ _db.getTLSSession() }; - auto transaction{ dbSession.createReadTransaction() }; + auto transaction{ createReadTransaction() }; - traceLogger->setMetadata("db_artist_count", std::to_string(db::Artist::getCount(dbSession))); - traceLogger->setMetadata("db_cluster_count", std::to_string(db::Cluster::getCount(dbSession))); - traceLogger->setMetadata("db_cluster_type_count", std::to_string(db::ClusterType::getCount(dbSession))); - traceLogger->setMetadata("db_starred_artist_count", std::to_string(db::StarredArtist::getCount(dbSession))); - traceLogger->setMetadata("db_starred_release_count", std::to_string(db::StarredRelease::getCount(dbSession))); - traceLogger->setMetadata("db_starred_track_count", std::to_string(db::StarredTrack::getCount(dbSession))); - traceLogger->setMetadata("db_track_bookmark_count", std::to_string(db::TrackBookmark::getCount(dbSession))); - traceLogger->setMetadata("db_listen_count", std::to_string(db::Listen::getCount(dbSession))); - traceLogger->setMetadata("db_release_count", std::to_string(db::Release::getCount(dbSession))); - traceLogger->setMetadata("db_track_count", std::to_string(db::Track::getCount(dbSession))); + traceLogger->setMetadata("db_artist_count", std::to_string(db::Artist::getCount(*this))); + traceLogger->setMetadata("db_cluster_count", std::to_string(db::Cluster::getCount(*this))); + traceLogger->setMetadata("db_cluster_type_count", std::to_string(db::ClusterType::getCount(*this))); + traceLogger->setMetadata("db_starred_artist_count", std::to_string(db::StarredArtist::getCount(*this))); + traceLogger->setMetadata("db_starred_release_count", std::to_string(db::StarredRelease::getCount(*this))); + traceLogger->setMetadata("db_starred_track_count", std::to_string(db::StarredTrack::getCount(*this))); + traceLogger->setMetadata("db_track_bookmark_count", std::to_string(db::TrackBookmark::getCount(*this))); + traceLogger->setMetadata("db_listen_count", std::to_string(db::Listen::getCount(*this))); + traceLogger->setMetadata("db_release_count", std::to_string(db::Release::getCount(*this))); + traceLogger->setMetadata("db_track_count", std::to_string(db::Track::getCount(*this))); } - void Session::analyze() + void Session::fullAnalyze() { - LMS_SCOPED_TRACE_DETAILED("Database", "Analyze"); - LMS_LOG(DB, INFO, "Analyzing database..."); + LMS_SCOPED_TRACE_OVERVIEW("Database", "Analyze"); + LMS_LOG(DB, INFO, "Performing database analyze... This may take a while..."); + + // first select all the tables and indexes, and then analyze one by one in order to not have a big lock + std::vector entries; + retrieveEntriesToAnalyze(entries); + + for (const std::string& entry : entries) + analyzeEntry(entry); + + LMS_LOG(DB, INFO, "Analyze complete!"); + } + + void Session::retrieveEntriesToAnalyze(std::vector& entryList) + { + auto transaction{ createReadTransaction() }; + entryList = utils::fetchQueryResults(_session.query("SELECT name FROM sqlite_master WHERE type='table' OR type ='index'")); + } + + void Session::analyzeEntry(const std::string& entry) + { + LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "AnalyzeEntry", "Entry", entry); + LMS_LOG(DB, DEBUG, "Analyzing " << entry); { auto transaction{ createWriteTransaction() }; - _session.execute("ANALYZE"); + _session.execute("ANALYZE " + entry); } - LMS_LOG(DB, INFO, "Database Analyze complete"); + LMS_LOG(DB, DEBUG, "Analyzing " << entry << ": done!"); } - - void Session::optimize() - { - LMS_SCOPED_TRACE_DETAILED("Database", "Optimize"); - LMS_LOG(DB, INFO, "Optimizing database..."); - { - auto transaction{ createWriteTransaction() }; - _session.execute("PRAGMA optimize"); - } - LMS_LOG(DB, INFO, "Database optimizing complete"); - } - } // namespace lms::db diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index d2d10083..2c8352af 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include "core/ITraceLogger.hpp" #include "core/RecursiveSharedMutex.hpp" #include "database/Object.hpp" @@ -84,10 +86,15 @@ namespace lms::db #endif } - void analyze(); - void optimize(); + // All these methods will acquire transactions + void fullAnalyze(); // helper for retrieveEntriesToAnalyze + analyzeEntry + void retrieveEntriesToAnalyze(std::vector& entryList); + void analyzeEntry(const std::string& entry); - void prepareTables(); // need to run only once at startup + void prepareTablesIfNeeded(); // need to run only once at startup + void migrateIfNeeded(); + void createIndexesIfNeeded(); + void vacuumIfNeeded(); void refreshTracingLoggerStats(); // returning a ptr here to ease further wrapping using operator-> diff --git a/src/libs/database/test/Common.cpp b/src/libs/database/test/Common.cpp index ac3089ab..d78a8557 100644 --- a/src/libs/database/test/Common.cpp +++ b/src/libs/database/test/Common.cpp @@ -60,8 +60,8 @@ namespace lms::db::tests _tmpDb = std::make_unique(); { db::Session s{ _tmpDb->getDb() }; - s.prepareTables(); - s.analyze(); + s.prepareTablesIfNeeded(); + s.createIndexesIfNeeded(); } } diff --git a/src/libs/services/scanner/CMakeLists.txt b/src/libs/services/scanner/CMakeLists.txt index 2993415a..4da8e7b8 100644 --- a/src/libs/services/scanner/CMakeLists.txt +++ b/src/libs/services/scanner/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(lmsscanner SHARED impl/ScannerService.cpp impl/ScannerStats.cpp + impl/ScanStepAnalyze.cpp impl/ScanStepCheckDuplicatedDbFiles.cpp impl/ScanStepComputeClusterStats.cpp impl/ScanStepDiscoverFiles.cpp diff --git a/src/libs/services/scanner/impl/IScanStep.hpp b/src/libs/services/scanner/impl/IScanStep.hpp index e131da4b..553a8203 100644 --- a/src/libs/services/scanner/impl/IScanStep.hpp +++ b/src/libs/services/scanner/impl/IScanStep.hpp @@ -19,9 +19,9 @@ #pragma once -#include #include +#include "core/LiteralString.hpp" #include "services/scanner/ScannerStats.hpp" namespace lms::scanner @@ -32,7 +32,7 @@ namespace lms::scanner virtual ~IScanStep() = default; virtual ScanStep getStep() const = 0; - virtual std::string_view getStepName() const = 0; + virtual core::LiteralString getStepName() const = 0; struct ScanContext { diff --git a/src/libs/services/scanner/impl/ScanStepAnalyze.cpp b/src/libs/services/scanner/impl/ScanStepAnalyze.cpp new file mode 100644 index 00000000..91731641 --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepAnalyze.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "ScanStepAnalyze.hpp" + +#include "core/ILogger.hpp" +#include "database/Db.hpp" +#include "database/Session.hpp" + +namespace lms::scanner +{ + void ScanStepAnalyze::process(ScanContext& context) + { + ScanStats& stats{ context.stats }; + + if (stats.nbChanges() > (stats.nbFiles() / 5)) + { + LMS_LOG(DBUPDATER, INFO, "Database changed substantially: triggering full analyze"); + + auto& session{ _db.getTLSSession() }; + + std::vector entries; + session.retrieveEntriesToAnalyze(entries); + context.currentStepStats.totalElems = entries.size(); + + for (const std::string& entry : entries) + { + if (_abortScan) + break; + + _db.getTLSSession().analyzeEntry(entry); + context.currentStepStats.processedElems++; + _progressCallback(context.currentStepStats); + } + } + } +} diff --git a/src/libs/services/scanner/impl/ScanStepAnalyze.hpp b/src/libs/services/scanner/impl/ScanStepAnalyze.hpp new file mode 100644 index 00000000..9a1763cb --- /dev/null +++ b/src/libs/services/scanner/impl/ScanStepAnalyze.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "ScanStepBase.hpp" + +namespace lms::scanner +{ + class ScanStepAnalyze : public ScanStepBase + { + public: + using ScanStepBase::ScanStepBase; + + private: + ScanStep getStep() const override { return ScanStep::Analyze; } + core::LiteralString getStepName() const override { return "Analyze"; } + void process(ScanContext& context) override; + }; +} diff --git a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp index 3599791d..ba02d7ac 100644 --- a/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepCheckDuplicatedDbFiles.hpp @@ -29,7 +29,7 @@ namespace lms::scanner using ScanStepBase::ScanStepBase; private: - std::string_view getStepName() const override { return "Checking for duplicated files"; } + core::LiteralString getStepName() const override { return "Checking for duplicated files"; } ScanStep getStep() const override { return ScanStep::CheckingForDuplicateFiles; } void process(ScanContext& context) override; }; diff --git a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp index a62d354e..331973fc 100644 --- a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.cpp @@ -79,6 +79,7 @@ namespace lms::scanner } context.currentStepStats.processedElems++; + _progressCallback(context.currentStepStats); } return true; diff --git a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp index 8000f454..43aeaf36 100644 --- a/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp +++ b/src/libs/services/scanner/impl/ScanStepComputeClusterStats.hpp @@ -30,7 +30,7 @@ namespace lms::scanner private: ScanStep getStep() const override { return ScanStep::ComputeClusterStats; } - std::string_view getStepName() const override { return "Compute cluster stats"; } + core::LiteralString getStepName() const override { return "Compute cluster stats"; } void process(ScanContext& context) override; }; } diff --git a/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp b/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp index 60f3e240..2a56a3b9 100644 --- a/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepDiscoverFiles.hpp @@ -30,7 +30,7 @@ namespace lms::scanner private: ScanStep getStep() const override { return ScanStep::DiscoveringFiles; } - std::string_view getStepName() const override { return "DiscoveringFiles"; } + core::LiteralString getStepName() const override { return "Discovering files"; } void process(ScanContext& context) override; }; } diff --git a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp index 5b56fd90..92c467ad 100644 --- a/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepRemoveOrphanDbFiles.hpp @@ -31,7 +31,7 @@ namespace lms::scanner using ScanStepBase::ScanStepBase; private: - std::string_view getStepName() const override { return "Checking orphaned entries"; } + core::LiteralString getStepName() const override { return "Checking orphaned entries"; } ScanStep getStep() const override { return ScanStep::ChekingForMissingFiles; } void process(ScanContext& context) override; diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp index cd2606a7..28bd7bc7 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.cpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.cpp @@ -518,10 +518,6 @@ namespace lms::scanner context.stats.scans++; processFileMetaData(context, scanResult.path, *scanResult.trackMetaData, libraryInfo); - - // optimize the database during scan (if we import a very large database, it may be too late to do it once at end) - if ((context.stats.scans % 1'000) == 0) - _db.getTLSSession().optimize(); } else { diff --git a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp index d656ea99..05398a39 100644 --- a/src/libs/services/scanner/impl/ScanStepScanFiles.hpp +++ b/src/libs/services/scanner/impl/ScanStepScanFiles.hpp @@ -40,7 +40,7 @@ namespace lms::scanner private: ScanStep getStep() const override { return ScanStep::ScanningFiles; } - std::string_view getStepName() const override { return "Scanning files"; } + core::LiteralString getStepName() const override { return "Scanning files"; } void process(ScanContext& context) override; bool checkFileNeedScan(ScanContext& context, const std::filesystem::path& file, const ScannerSettings::MediaLibraryInfo& libraryInfo); diff --git a/src/libs/services/scanner/impl/ScannerService.cpp b/src/libs/services/scanner/impl/ScannerService.cpp index 356cbcbb..fc82af62 100644 --- a/src/libs/services/scanner/impl/ScannerService.cpp +++ b/src/libs/services/scanner/impl/ScannerService.cpp @@ -25,15 +25,17 @@ #include "database/TrackFeatures.hpp" #include "database/ScanSettings.hpp" #include "core/Exception.hpp" +#include "core/Path.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" -#include "core/Path.hpp" +#include "core/ITraceLogger.hpp" +#include "ScanStepAnalyze.hpp" #include "ScanStepCheckDuplicatedDbFiles.hpp" +#include "ScanStepComputeClusterStats.hpp" #include "ScanStepDiscoverFiles.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepScanFiles.hpp" -#include "ScanStepComputeClusterStats.hpp" namespace lms::scanner { @@ -259,6 +261,8 @@ namespace lms::scanner void ScannerService::scan(bool forceScan) { + LMS_SCOPED_TRACE_OVERVIEW("Scanner", "Scan"); + _events.scanStarted.emit(); { @@ -278,6 +282,8 @@ namespace lms::scanner for (auto& scanStep : _scanSteps) { + LMS_SCOPED_TRACE_OVERVIEW("Scanner", scanStep->getStepName()); + LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'"); scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() }; @@ -289,8 +295,6 @@ namespace lms::scanner LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size()); - _db.getTLSSession().analyze(); - if (!_abortScan) { stats.stopTime = Wt::WDateTime::currentDateTime(); @@ -348,6 +352,7 @@ namespace lms::scanner _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); _scanSteps.push_back(std::make_unique(params)); + _scanSteps.push_back(std::make_unique(params)); } ScannerSettings ScannerService::readSettings() diff --git a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp index 02094980..ed5dffe8 100644 --- a/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp +++ b/src/libs/services/scanner/include/services/scanner/ScannerStats.hpp @@ -66,8 +66,9 @@ namespace lms::scanner FetchingTrackFeatures, ReloadingSimilarityEngine, ComputeClusterStats, + Analyze, }; - static inline constexpr unsigned ScanProgressStepCount{ 7 }; + static inline constexpr unsigned ScanProgressStepCount{ 8 }; // reduced scan stats struct ScanStepStats diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index a21e8098..2a877826 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable(lms ui/Auth.cpp ui/LmsApplication.cpp ui/LmsApplicationManager.cpp + ui/LmsInitApplication.cpp ui/LmsTheme.cpp ui/MediaPlayer.cpp ui/ModalManager.cpp diff --git a/src/lms/main.cpp b/src/lms/main.cpp index dd6c684c..dfe1f0b8 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -40,6 +40,7 @@ #include "subsonic/SubsonicResource.hpp" #include "ui/LmsApplication.hpp" #include "ui/LmsApplicationManager.hpp" +#include "ui/LmsInitApplication.hpp" #include "core/IChildProcessManager.hpp" #include "core/IConfig.hpp" #include "core/IOContextRunner.hpp" @@ -275,20 +276,34 @@ namespace lms Wt::WServer server{ argv[0] }; server.setServerConfiguration(wtServerArgs.size(), const_cast(&wtArgv[0])); + // As initialization can take a while (db migration, analyze, etc.), we bind a temporary init entry point to warn the user + server.addEntryPoint(Wt::EntryPointType::Application, + [&](const Wt::WEnvironment& env) + { + return ui::LmsInitApplication::create(env); + }); + + LMS_LOG(MAIN, INFO, "Starting init web server..."); + server.start(); + core::IOContextRunner ioContextRunner{ ioContext, getThreadCount(), "Misc" }; // Connection pool size must be twice the number of threads: we have at least 2 io pools with getThreadCount() each and they all may access the database db::Db database{ config->getPath("working-dir") / "lms.db", getThreadCount() * 2 }; { db::Session session{ database }; - session.prepareTables(); + session.prepareTablesIfNeeded(); + session.createIndexesIfNeeded(); + + // As this may be quite long, we only do it during startup + session.vacuumIfNeeded(); // force optimize in case scanner aborted during a large import: - // queries may be too slow to even be able to relaunch a scan sing the web interface - session.analyze(); + // queries may be too slow to even be able to relaunch a scan using the web interface + session.fullAnalyze(); database.getTLSSession().refreshTracingLoggerStats(); } - + ui::LmsApplicationManager appManager; // Service initialization order is important (reverse-order for deinit) @@ -327,8 +342,12 @@ namespace lms core::Service feedbackService{ feedback::createFeedbackService(ioContext, database) }; core::Service scrobblingService{ scrobbling::createScrobblingService(ioContext, database) }; - std::unique_ptr subsonicResource; + LMS_LOG(MAIN, INFO, "Stopping init web server..."); + server.stop(); + server.removeEntryPoint(""); + + std::unique_ptr subsonicResource; // bind API resources if (config->getBool("api-subsonic", true)) { @@ -345,7 +364,7 @@ namespace lms proxyScannerEventsToApplication(*scannerService, server); - LMS_LOG(MAIN, INFO, "Starting server..."); + LMS_LOG(MAIN, INFO, "Starting init web server..."); server.start(); LMS_LOG(MAIN, INFO, "Now running..."); diff --git a/src/lms/ui/LmsInitApplication.cpp b/src/lms/ui/LmsInitApplication.cpp new file mode 100644 index 00000000..4870b6ee --- /dev/null +++ b/src/lms/ui/LmsInitApplication.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include + +#include "LmsInitApplication.hpp" + +namespace lms::ui +{ + LmsInitApplication::LmsInitApplication(const Wt::WEnvironment& env) + : Wt::WApplication{ env } + { + enableUpdates(true); + root()->addNew("LMS is initializing. This may take a while, please wait..."); + } + + std::unique_ptr LmsInitApplication::create(const Wt::WEnvironment& env) + { + return std::make_unique(env); + } +} // namespace lms::ui \ No newline at end of file diff --git a/src/lms/ui/LmsInitApplication.hpp b/src/lms/ui/LmsInitApplication.hpp new file mode 100644 index 00000000..2e6d4a8d --- /dev/null +++ b/src/lms/ui/LmsInitApplication.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace lms::ui +{ + class LmsInitApplication : public Wt::WApplication + { + public: + LmsInitApplication(const Wt::WEnvironment& env); + + static std::unique_ptr create(const Wt::WEnvironment& env); + }; +} // namespace lms::ui \ No newline at end of file diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 65f08fd6..d7eb29a7 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -274,6 +274,13 @@ namespace lms::ui case scanner::ScanStep::ComputeClusterStats: _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-compute-cluster-stats") .arg(status.currentScanStepStats->progress())); + + case scanner::ScanStep:: Analyze: + _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-analyze") + .arg(status.currentScanStepStats->processedElems) + .arg(status.currentScanStepStats->totalElems) + .arg(status.currentScanStepStats->progress())); + break; } break; }