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