/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/DebugOnly.h" #include "VacuumManager.h" #include "mozilla/Services.h" #include "mozilla/Preferences.h" #include "nsIObserverService.h" #include "nsIFile.h" #include "nsThreadUtils.h" #include "mozilla/Logging.h" #include "prtime.h" #include "mozilla/StaticPrefs_storage.h" #include "mozStorageConnection.h" #include "mozIStorageStatement.h" #include "mozIStorageStatementCallback.h" #include "mozIStorageAsyncStatement.h" #include "mozIStoragePendingStatement.h" #include "mozIStorageError.h" #include "mozStorageHelper.h" #include "nsXULAppAPI.h" #define OBSERVER_TOPIC_IDLE_DAILY "idle-daily" #define OBSERVER_TOPIC_XPCOM_SHUTDOWN "xpcom-shutdown" // Used to notify begin and end of a heavy IO task. #define OBSERVER_TOPIC_HEAVY_IO "heavy-io-task" #define OBSERVER_DATA_VACUUM_BEGIN u"vacuum-begin" #define OBSERVER_DATA_VACUUM_END u"vacuum-end" // This preferences root will contain last vacuum timestamps (in seconds) for // each database. The database filename is used as a key. #define PREF_VACUUM_BRANCH "storage.vacuum.last." // Time between subsequent vacuum calls for a certain database. #define VACUUM_INTERVAL_SECONDS 30 * 86400 // 30 days. extern mozilla::LazyLogModule gStorageLog; namespace mozilla { namespace storage { namespace { //////////////////////////////////////////////////////////////////////////////// //// BaseCallback class BaseCallback : public mozIStorageStatementCallback { public: NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGESTATEMENTCALLBACK BaseCallback() {} protected: virtual ~BaseCallback() {} }; NS_IMETHODIMP BaseCallback::HandleError(mozIStorageError* aError) { #ifdef DEBUG int32_t result; nsresult rv = aError->GetResult(&result); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString message; rv = aError->GetMessage(message); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString warnMsg; warnMsg.AppendLiteral("An error occured during async execution: "); warnMsg.AppendInt(result); warnMsg.Append(' '); warnMsg.Append(message); NS_WARNING(warnMsg.get()); #endif return NS_OK; } NS_IMETHODIMP BaseCallback::HandleResult(mozIStorageResultSet* aResultSet) { // We could get results from PRAGMA statements, but we don't mind them. return NS_OK; } NS_IMETHODIMP BaseCallback::HandleCompletion(uint16_t aReason) { // By default BaseCallback will just be silent on completion. return NS_OK; } NS_IMPL_ISUPPORTS(BaseCallback, mozIStorageStatementCallback) //////////////////////////////////////////////////////////////////////////////// //// Vacuumer declaration. class Vacuumer : public BaseCallback { public: NS_DECL_MOZISTORAGESTATEMENTCALLBACK explicit Vacuumer(mozIStorageVacuumParticipant* aParticipant); bool execute(); nsresult notifyCompletion(bool aSucceeded); private: nsCOMPtr mParticipant; nsCString mDBFilename; nsCOMPtr mDBConn; }; //////////////////////////////////////////////////////////////////////////////// //// Vacuumer implementation. Vacuumer::Vacuumer(mozIStorageVacuumParticipant* aParticipant) : mParticipant(aParticipant) {} bool Vacuumer::execute() { MOZ_ASSERT(NS_IsMainThread(), "Must be running on the main thread!"); // Get the connection and check its validity. nsresult rv = mParticipant->GetDatabaseConnection(getter_AddRefs(mDBConn)); NS_ENSURE_SUCCESS(rv, false); bool ready = false; if (!mDBConn || NS_FAILED(mDBConn->GetConnectionReady(&ready)) || !ready) { NS_WARNING("Unable to get a connection to vacuum database"); return false; } // Ask for the expected page size. Vacuum can change the page size, unless // the database is using WAL journaling. // TODO Bug 634374: figure out a strategy to fix page size with WAL. int32_t expectedPageSize = 0; rv = mParticipant->GetExpectedDatabasePageSize(&expectedPageSize); if (NS_FAILED(rv) || !Service::pageSizeIsValid(expectedPageSize)) { NS_WARNING("Invalid page size requested for database, will use default "); NS_WARNING(mDBFilename.get()); expectedPageSize = Service::kDefaultPageSize; } // Get the database filename. Last vacuum time is stored under this name // in PREF_VACUUM_BRANCH. nsCOMPtr databaseFile; mDBConn->GetDatabaseFile(getter_AddRefs(databaseFile)); if (!databaseFile) { NS_WARNING("Trying to vacuum a in-memory database!"); return false; } nsAutoString databaseFilename; rv = databaseFile->GetLeafName(databaseFilename); NS_ENSURE_SUCCESS(rv, false); CopyUTF16toUTF8(databaseFilename, mDBFilename); MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); // Check interval from last vacuum. int32_t now = static_cast(PR_Now() / PR_USEC_PER_SEC); int32_t lastVacuum; nsAutoCString prefName(PREF_VACUUM_BRANCH); prefName += mDBFilename; rv = Preferences::GetInt(prefName.get(), &lastVacuum); if (NS_SUCCEEDED(rv) && (now - lastVacuum) < VACUUM_INTERVAL_SECONDS) { // This database was vacuumed recently, skip it. return false; } // Notify that we are about to start vacuuming. The participant can opt-out // if it cannot handle a vacuum at this time, and then we'll move to the next // one. bool vacuumGranted = false; rv = mParticipant->OnBeginVacuum(&vacuumGranted); NS_ENSURE_SUCCESS(rv, false); if (!vacuumGranted) { return false; } // Notify a heavy IO task is about to start. nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { rv = os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO, OBSERVER_DATA_VACUUM_BEGIN); MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to notify"); } // Execute the statements separately, since the pragma may conflict with the // vacuum, if they are executed in the same transaction. nsCOMPtr pageSizeStmt; nsAutoCString pageSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA page_size = "); pageSizeQuery.AppendInt(expectedPageSize); rv = mDBConn->CreateAsyncStatement(pageSizeQuery, getter_AddRefs(pageSizeStmt)); NS_ENSURE_SUCCESS(rv, false); RefPtr callback = new BaseCallback(); nsCOMPtr ps; rv = pageSizeStmt->ExecuteAsync(callback, getter_AddRefs(ps)); NS_ENSURE_SUCCESS(rv, false); nsCOMPtr stmt; rv = mDBConn->CreateAsyncStatement("VACUUM"_ns, getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, false); rv = stmt->ExecuteAsync(this, getter_AddRefs(ps)); NS_ENSURE_SUCCESS(rv, false); return true; } //////////////////////////////////////////////////////////////////////////////// //// mozIStorageStatementCallback NS_IMETHODIMP Vacuumer::HandleError(mozIStorageError* aError) { int32_t result; nsresult rv; nsAutoCString message; #ifdef DEBUG rv = aError->GetResult(&result); NS_ENSURE_SUCCESS(rv, rv); rv = aError->GetMessage(message); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString warnMsg; warnMsg.AppendLiteral("Unable to vacuum database: "); warnMsg.Append(mDBFilename); warnMsg.AppendLiteral(" - "); warnMsg.AppendInt(result); warnMsg.Append(' '); warnMsg.Append(message); NS_WARNING(warnMsg.get()); #endif if (MOZ_LOG_TEST(gStorageLog, LogLevel::Error)) { rv = aError->GetResult(&result); NS_ENSURE_SUCCESS(rv, rv); rv = aError->GetMessage(message); NS_ENSURE_SUCCESS(rv, rv); MOZ_LOG(gStorageLog, LogLevel::Error, ("Vacuum failed with error: %d '%s'. Database was: '%s'", result, message.get(), mDBFilename.get())); } return NS_OK; } NS_IMETHODIMP Vacuumer::HandleResult(mozIStorageResultSet* aResultSet) { MOZ_ASSERT_UNREACHABLE("Got a resultset from a vacuum?"); return NS_OK; } NS_IMETHODIMP Vacuumer::HandleCompletion(uint16_t aReason) { if (aReason == REASON_FINISHED) { // Update last vacuum time. int32_t now = static_cast(PR_Now() / PR_USEC_PER_SEC); MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); nsAutoCString prefName(PREF_VACUUM_BRANCH); prefName += mDBFilename; DebugOnly rv = Preferences::SetInt(prefName.get(), now); MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); } notifyCompletion(aReason == REASON_FINISHED); return NS_OK; } nsresult Vacuumer::notifyCompletion(bool aSucceeded) { nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO, OBSERVER_DATA_VACUUM_END); } nsresult rv = mParticipant->OnEndVacuum(aSucceeded); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } } // namespace //////////////////////////////////////////////////////////////////////////////// //// VacuumManager NS_IMPL_ISUPPORTS(VacuumManager, nsIObserver) VacuumManager* VacuumManager::gVacuumManager = nullptr; already_AddRefed VacuumManager::getSingleton() { // Don't allocate it in the child Process. if (!XRE_IsParentProcess()) { return nullptr; } if (!gVacuumManager) { auto manager = MakeRefPtr(); MOZ_ASSERT(gVacuumManager == manager.get()); return manager.forget(); } return do_AddRef(gVacuumManager); } VacuumManager::VacuumManager() : mParticipants("vacuum-participant") { MOZ_ASSERT(!gVacuumManager, "Attempting to create two instances of the service!"); gVacuumManager = this; } VacuumManager::~VacuumManager() { // Remove the static reference to the service. Check to make sure its us // in case somebody creates an extra instance of the service. MOZ_ASSERT(gVacuumManager == this, "Deleting a non-singleton instance of the service"); if (gVacuumManager == this) { gVacuumManager = nullptr; } } //////////////////////////////////////////////////////////////////////////////// //// nsIObserver NS_IMETHODIMP VacuumManager::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY) == 0) { // Try to run vacuum on all registered entries. Will stop at the first // successful one. nsCOMArray entries; mParticipants.GetEntries(entries); // If there are more entries than what a month can contain, we could end up // skipping some, since we run daily. So we use a starting index. static const char* kPrefName = PREF_VACUUM_BRANCH "index"; int32_t startIndex = Preferences::GetInt(kPrefName, 0); if (startIndex >= entries.Count()) { startIndex = 0; } int32_t index; for (index = startIndex; index < entries.Count(); ++index) { RefPtr vacuum = new Vacuumer(entries[index]); // Only vacuum one database per day. if (vacuum->execute()) { break; } } DebugOnly rv = Preferences::SetInt(kPrefName, index); MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); } return NS_OK; } } // namespace storage } // namespace mozilla