VacuumManager.cpp (9345B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/DebugOnly.h" 8 9 #include "VacuumManager.h" 10 11 #include "mozilla/ErrorNames.h" 12 #include "mozilla/Services.h" 13 #include "mozilla/Preferences.h" 14 #include "nsIObserverService.h" 15 #include "nsIFile.h" 16 #include "nsThreadUtils.h" 17 #include "mozilla/Logging.h" 18 #include "mozilla/IntegerPrintfMacros.h" 19 #include "prtime.h" 20 21 #include "mozStorageConnection.h" 22 #include "mozStoragePrivateHelpers.h" 23 #include "mozIStorageCompletionCallback.h" 24 #include "nsXULAppAPI.h" 25 #include "xpcpublic.h" 26 27 #define OBSERVER_TOPIC_IDLE_DAILY "idle-daily" 28 29 // Used to notify the begin and end of a vacuum operation. 30 #define OBSERVER_TOPIC_VACUUM_BEGIN "vacuum-begin" 31 #define OBSERVER_TOPIC_VACUUM_END "vacuum-end" 32 // This notification is sent only in automation when vacuum for a database is 33 // skipped, and can thus be used to verify that. 34 #define OBSERVER_TOPIC_VACUUM_SKIP "vacuum-skip" 35 36 // This preferences root will contain last vacuum timestamps (in seconds) for 37 // each database. The database filename is used as a key. 38 #define PREF_VACUUM_BRANCH "storage.vacuum.last." 39 40 // Time between subsequent vacuum calls for a certain database. 41 #define VACUUM_INTERVAL_SECONDS (30 * 86400) // 30 days. 42 43 extern mozilla::LazyLogModule gStorageLog; 44 45 namespace mozilla::storage { 46 47 namespace { 48 49 //////////////////////////////////////////////////////////////////////////////// 50 //// Vacuumer declaration. 51 52 class Vacuumer final : public mozIStorageCompletionCallback { 53 public: 54 NS_DECL_ISUPPORTS 55 NS_DECL_MOZISTORAGECOMPLETIONCALLBACK 56 57 explicit Vacuumer(mozIStorageVacuumParticipant* aParticipant); 58 bool execute(); 59 60 private: 61 nsresult notifyCompletion(bool aSucceeded); 62 ~Vacuumer() = default; 63 64 nsCOMPtr<mozIStorageVacuumParticipant> mParticipant; 65 nsCString mDBFilename; 66 nsCOMPtr<mozIStorageAsyncConnection> mDBConn; 67 }; 68 69 //////////////////////////////////////////////////////////////////////////////// 70 //// Vacuumer implementation. 71 72 NS_IMPL_ISUPPORTS(Vacuumer, mozIStorageCompletionCallback) 73 74 Vacuumer::Vacuumer(mozIStorageVacuumParticipant* aParticipant) 75 : mParticipant(aParticipant) {} 76 77 bool Vacuumer::execute() { 78 MOZ_ASSERT(NS_IsMainThread(), "Must be running on the main thread!"); 79 80 // Get the connection and check its validity. 81 nsresult rv = mParticipant->GetDatabaseConnection(getter_AddRefs(mDBConn)); 82 if (NS_FAILED(rv) || !mDBConn) return false; 83 84 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); 85 86 bool inAutomation = xpc::IsInAutomation(); 87 // Get the database filename. Last vacuum time is stored under this name 88 // in PREF_VACUUM_BRANCH. 89 nsCOMPtr<nsIFile> databaseFile; 90 mDBConn->GetDatabaseFile(getter_AddRefs(databaseFile)); 91 if (!databaseFile) { 92 NS_WARNING("Trying to vacuum a in-memory database!"); 93 if (inAutomation && os) { 94 (void)os->NotifyObservers(nullptr, OBSERVER_TOPIC_VACUUM_SKIP, 95 u":memory:"); 96 } 97 return false; 98 } 99 nsAutoString databaseFilename; 100 rv = databaseFile->GetLeafName(databaseFilename); 101 NS_ENSURE_SUCCESS(rv, false); 102 CopyUTF16toUTF8(databaseFilename, mDBFilename); 103 MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); 104 105 // Check interval from last vacuum. 106 int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC); 107 int32_t lastVacuum; 108 nsAutoCString prefName(PREF_VACUUM_BRANCH); 109 prefName += mDBFilename; 110 rv = Preferences::GetInt(prefName.get(), &lastVacuum); 111 if (NS_SUCCEEDED(rv) && (now - lastVacuum) < VACUUM_INTERVAL_SECONDS) { 112 // This database was vacuumed recently, skip it. 113 if (inAutomation && os) { 114 (void)os->NotifyObservers(nullptr, OBSERVER_TOPIC_VACUUM_SKIP, 115 NS_ConvertUTF8toUTF16(mDBFilename).get()); 116 } 117 return false; 118 } 119 120 // Notify that we are about to start vacuuming. The participant can opt-out 121 // if it cannot handle a vacuum at this time, and then we'll move to the next 122 // one. 123 bool vacuumGranted = false; 124 rv = mParticipant->OnBeginVacuum(&vacuumGranted); 125 NS_ENSURE_SUCCESS(rv, false); 126 if (!vacuumGranted) { 127 if (inAutomation && os) { 128 (void)os->NotifyObservers(nullptr, OBSERVER_TOPIC_VACUUM_SKIP, 129 NS_ConvertUTF8toUTF16(mDBFilename).get()); 130 } 131 return false; 132 } 133 134 // Ask for the expected page size. Vacuum can change the page size, unless 135 // the database is using WAL journaling. 136 // TODO Bug 634374: figure out a strategy to fix page size with WAL. 137 int32_t expectedPageSize = 0; 138 rv = mParticipant->GetExpectedDatabasePageSize(&expectedPageSize); 139 if (NS_FAILED(rv) || !Service::pageSizeIsValid(expectedPageSize)) { 140 NS_WARNING("Invalid page size requested for database, won't set it. "); 141 NS_WARNING(mDBFilename.get()); 142 expectedPageSize = 0; 143 } 144 145 bool incremental = false; 146 (void)mParticipant->GetUseIncrementalVacuum(&incremental); 147 148 // Notify vacuum is about to start. 149 if (os) { 150 (void)os->NotifyObservers(nullptr, OBSERVER_TOPIC_VACUUM_BEGIN, 151 NS_ConvertUTF8toUTF16(mDBFilename).get()); 152 } 153 154 rv = mDBConn->AsyncVacuum(this, incremental, expectedPageSize); 155 if (NS_FAILED(rv)) { 156 // The connection is not ready. 157 (void)Complete(rv, nullptr); 158 return false; 159 } 160 161 return true; 162 } 163 164 NS_IMETHODIMP 165 Vacuumer::Complete(nsresult aStatus, nsISupports* aValue) { 166 if (NS_SUCCEEDED(aStatus)) { 167 // Update last vacuum time. 168 int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC); 169 MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); 170 nsAutoCString prefName(PREF_VACUUM_BRANCH); 171 prefName += mDBFilename; 172 DebugOnly<nsresult> rv = Preferences::SetInt(prefName.get(), now); 173 MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); 174 notifyCompletion(true); 175 return NS_OK; 176 } 177 178 nsAutoCString errName; 179 GetErrorName(aStatus, errName); 180 nsCString errMsg = nsPrintfCString( 181 "Vacuum failed on '%s' with error %s - code %" PRIX32, mDBFilename.get(), 182 errName.get(), static_cast<uint32_t>(aStatus)); 183 NS_WARNING(errMsg.get()); 184 if (MOZ_LOG_TEST(gStorageLog, LogLevel::Error)) { 185 MOZ_LOG(gStorageLog, LogLevel::Error, ("%s", errMsg.get())); 186 } 187 188 notifyCompletion(false); 189 return NS_OK; 190 } 191 192 nsresult Vacuumer::notifyCompletion(bool aSucceeded) { 193 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); 194 if (os) { 195 (void)os->NotifyObservers(nullptr, OBSERVER_TOPIC_VACUUM_END, 196 NS_ConvertUTF8toUTF16(mDBFilename).get()); 197 } 198 199 nsresult rv = mParticipant->OnEndVacuum(aSucceeded); 200 NS_ENSURE_SUCCESS(rv, rv); 201 202 return NS_OK; 203 } 204 205 } // namespace 206 207 //////////////////////////////////////////////////////////////////////////////// 208 //// VacuumManager 209 210 NS_IMPL_ISUPPORTS(VacuumManager, nsIObserver) 211 212 VacuumManager* VacuumManager::gVacuumManager = nullptr; 213 214 already_AddRefed<VacuumManager> VacuumManager::getSingleton() { 215 // Don't allocate it in the child Process. 216 if (!XRE_IsParentProcess()) { 217 return nullptr; 218 } 219 220 if (!gVacuumManager) { 221 auto manager = MakeRefPtr<VacuumManager>(); 222 MOZ_ASSERT(gVacuumManager == manager.get()); 223 return manager.forget(); 224 } 225 return do_AddRef(gVacuumManager); 226 } 227 228 VacuumManager::VacuumManager() : mParticipants("vacuum-participant") { 229 MOZ_ASSERT(!gVacuumManager, 230 "Attempting to create two instances of the service!"); 231 gVacuumManager = this; 232 } 233 234 VacuumManager::~VacuumManager() { 235 // Remove the static reference to the service. Check to make sure its us 236 // in case somebody creates an extra instance of the service. 237 MOZ_ASSERT(gVacuumManager == this, 238 "Deleting a non-singleton instance of the service"); 239 if (gVacuumManager == this) { 240 gVacuumManager = nullptr; 241 } 242 } 243 244 //////////////////////////////////////////////////////////////////////////////// 245 //// nsIObserver 246 247 NS_IMETHODIMP 248 VacuumManager::Observe(nsISupports* aSubject, const char* aTopic, 249 const char16_t* aData) { 250 if (strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY) == 0) { 251 // Try to run vacuum on all registered entries. Will stop at the first 252 // successful one. 253 nsCOMArray<mozIStorageVacuumParticipant> entries; 254 mParticipants.GetEntries(entries); 255 // If there are more entries than what a month can contain, we could end up 256 // skipping some, since we run daily. So we use a starting index. 257 static const char* kPrefName = PREF_VACUUM_BRANCH "index"; 258 int32_t startIndex = Preferences::GetInt(kPrefName, 0); 259 if (startIndex >= entries.Count()) { 260 startIndex = 0; 261 } 262 int32_t index; 263 for (index = startIndex; index < entries.Count(); ++index) { 264 RefPtr<Vacuumer> vacuum = new Vacuumer(entries[index]); 265 // Only vacuum one database per day. 266 if (vacuum->execute()) { 267 break; 268 } 269 } 270 DebugOnly<nsresult> rv = Preferences::SetInt(kPrefName, index); 271 MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); 272 } 273 274 return NS_OK; 275 } 276 277 } // namespace mozilla::storage