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/Services.h"
12 #include "mozilla/Preferences.h"
13 #include "nsIObserverService.h"
14 #include "nsIFile.h"
15 #include "nsThreadUtils.h"
16 #include "mozilla/Logging.h"
17 #include "prtime.h"
18
19 #include "mozStorageConnection.h"
20 #include "mozIStorageStatement.h"
21 #include "mozIStorageAsyncStatement.h"
22 #include "mozIStoragePendingStatement.h"
23 #include "mozIStorageError.h"
24 #include "mozStorageHelper.h"
25 #include "nsXULAppAPI.h"
26
27 #define OBSERVER_TOPIC_IDLE_DAILY "idle-daily"
28 #define OBSERVER_TOPIC_XPCOM_SHUTDOWN "xpcom-shutdown"
29
30 // Used to notify begin and end of a heavy IO task.
31 #define OBSERVER_TOPIC_HEAVY_IO "heavy-io-task"
32 #define OBSERVER_DATA_VACUUM_BEGIN u"vacuum-begin"
33 #define OBSERVER_DATA_VACUUM_END u"vacuum-end"
34
35 // This preferences root will contain last vacuum timestamps (in seconds) for
36 // each database. The database filename is used as a key.
37 #define PREF_VACUUM_BRANCH "storage.vacuum.last."
38
39 // Time between subsequent vacuum calls for a certain database.
40 #define VACUUM_INTERVAL_SECONDS 30 * 86400 // 30 days.
41
42 extern mozilla::LazyLogModule gStorageLog;
43
44 namespace mozilla {
45 namespace storage {
46
47 namespace {
48
49 ////////////////////////////////////////////////////////////////////////////////
50 //// BaseCallback
51
52 class BaseCallback : public mozIStorageStatementCallback {
53 public:
54 NS_DECL_ISUPPORTS
55 NS_DECL_MOZISTORAGESTATEMENTCALLBACK
BaseCallback()56 BaseCallback() {}
57
58 protected:
~BaseCallback()59 virtual ~BaseCallback() {}
60 };
61
62 NS_IMETHODIMP
HandleError(mozIStorageError * aError)63 BaseCallback::HandleError(mozIStorageError *aError) {
64 #ifdef DEBUG
65 int32_t result;
66 nsresult rv = aError->GetResult(&result);
67 NS_ENSURE_SUCCESS(rv, rv);
68 nsAutoCString message;
69 rv = aError->GetMessage(message);
70 NS_ENSURE_SUCCESS(rv, rv);
71
72 nsAutoCString warnMsg;
73 warnMsg.AppendLiteral("An error occured during async execution: ");
74 warnMsg.AppendInt(result);
75 warnMsg.Append(' ');
76 warnMsg.Append(message);
77 NS_WARNING(warnMsg.get());
78 #endif
79 return NS_OK;
80 }
81
82 NS_IMETHODIMP
HandleResult(mozIStorageResultSet * aResultSet)83 BaseCallback::HandleResult(mozIStorageResultSet *aResultSet) {
84 // We could get results from PRAGMA statements, but we don't mind them.
85 return NS_OK;
86 }
87
88 NS_IMETHODIMP
HandleCompletion(uint16_t aReason)89 BaseCallback::HandleCompletion(uint16_t aReason) {
90 // By default BaseCallback will just be silent on completion.
91 return NS_OK;
92 }
93
94 NS_IMPL_ISUPPORTS(BaseCallback, mozIStorageStatementCallback)
95
96 ////////////////////////////////////////////////////////////////////////////////
97 //// Vacuumer declaration.
98
99 class Vacuumer : public BaseCallback {
100 public:
101 NS_DECL_MOZISTORAGESTATEMENTCALLBACK
102
103 explicit Vacuumer(mozIStorageVacuumParticipant *aParticipant);
104
105 bool execute();
106 nsresult notifyCompletion(bool aSucceeded);
107
108 private:
109 nsCOMPtr<mozIStorageVacuumParticipant> mParticipant;
110 nsCString mDBFilename;
111 nsCOMPtr<mozIStorageConnection> mDBConn;
112 };
113
114 ////////////////////////////////////////////////////////////////////////////////
115 //// Vacuumer implementation.
116
Vacuumer(mozIStorageVacuumParticipant * aParticipant)117 Vacuumer::Vacuumer(mozIStorageVacuumParticipant *aParticipant)
118 : mParticipant(aParticipant) {}
119
execute()120 bool Vacuumer::execute() {
121 MOZ_ASSERT(NS_IsMainThread(), "Must be running on the main thread!");
122
123 // Get the connection and check its validity.
124 nsresult rv = mParticipant->GetDatabaseConnection(getter_AddRefs(mDBConn));
125 NS_ENSURE_SUCCESS(rv, false);
126 bool ready = false;
127 if (!mDBConn || NS_FAILED(mDBConn->GetConnectionReady(&ready)) || !ready) {
128 NS_WARNING("Unable to get a connection to vacuum database");
129 return false;
130 }
131
132 // Ask for the expected page size. Vacuum can change the page size, unless
133 // the database is using WAL journaling.
134 // TODO Bug 634374: figure out a strategy to fix page size with WAL.
135 int32_t expectedPageSize = 0;
136 rv = mParticipant->GetExpectedDatabasePageSize(&expectedPageSize);
137 if (NS_FAILED(rv) || !Service::pageSizeIsValid(expectedPageSize)) {
138 NS_WARNING("Invalid page size requested for database, will use default ");
139 NS_WARNING(mDBFilename.get());
140 expectedPageSize = Service::getDefaultPageSize();
141 }
142
143 // Get the database filename. Last vacuum time is stored under this name
144 // in PREF_VACUUM_BRANCH.
145 nsCOMPtr<nsIFile> databaseFile;
146 mDBConn->GetDatabaseFile(getter_AddRefs(databaseFile));
147 if (!databaseFile) {
148 NS_WARNING("Trying to vacuum a in-memory database!");
149 return false;
150 }
151 nsAutoString databaseFilename;
152 rv = databaseFile->GetLeafName(databaseFilename);
153 NS_ENSURE_SUCCESS(rv, false);
154 mDBFilename = NS_ConvertUTF16toUTF8(databaseFilename);
155 MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty");
156
157 // Check interval from last vacuum.
158 int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC);
159 int32_t lastVacuum;
160 nsAutoCString prefName(PREF_VACUUM_BRANCH);
161 prefName += mDBFilename;
162 rv = Preferences::GetInt(prefName.get(), &lastVacuum);
163 if (NS_SUCCEEDED(rv) && (now - lastVacuum) < VACUUM_INTERVAL_SECONDS) {
164 // This database was vacuumed recently, skip it.
165 return false;
166 }
167
168 // Notify that we are about to start vacuuming. The participant can opt-out
169 // if it cannot handle a vacuum at this time, and then we'll move to the next
170 // one.
171 bool vacuumGranted = false;
172 rv = mParticipant->OnBeginVacuum(&vacuumGranted);
173 NS_ENSURE_SUCCESS(rv, false);
174 if (!vacuumGranted) {
175 return false;
176 }
177
178 // Notify a heavy IO task is about to start.
179 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
180 if (os) {
181 rv = os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO,
182 OBSERVER_DATA_VACUUM_BEGIN);
183 MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to notify");
184 }
185
186 // Execute the statements separately, since the pragma may conflict with the
187 // vacuum, if they are executed in the same transaction.
188 nsCOMPtr<mozIStorageAsyncStatement> pageSizeStmt;
189 nsAutoCString pageSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR
190 "PRAGMA page_size = ");
191 pageSizeQuery.AppendInt(expectedPageSize);
192 rv = mDBConn->CreateAsyncStatement(pageSizeQuery,
193 getter_AddRefs(pageSizeStmt));
194 NS_ENSURE_SUCCESS(rv, false);
195 RefPtr<BaseCallback> callback = new BaseCallback();
196 nsCOMPtr<mozIStoragePendingStatement> ps;
197 rv = pageSizeStmt->ExecuteAsync(callback, getter_AddRefs(ps));
198 NS_ENSURE_SUCCESS(rv, false);
199
200 nsCOMPtr<mozIStorageAsyncStatement> stmt;
201 rv = mDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING("VACUUM"),
202 getter_AddRefs(stmt));
203 NS_ENSURE_SUCCESS(rv, false);
204 rv = stmt->ExecuteAsync(this, getter_AddRefs(ps));
205 NS_ENSURE_SUCCESS(rv, false);
206
207 return true;
208 }
209
210 ////////////////////////////////////////////////////////////////////////////////
211 //// mozIStorageStatementCallback
212
213 NS_IMETHODIMP
HandleError(mozIStorageError * aError)214 Vacuumer::HandleError(mozIStorageError *aError) {
215 int32_t result;
216 nsresult rv;
217 nsAutoCString message;
218
219 #ifdef DEBUG
220 rv = aError->GetResult(&result);
221 NS_ENSURE_SUCCESS(rv, rv);
222 rv = aError->GetMessage(message);
223 NS_ENSURE_SUCCESS(rv, rv);
224
225 nsAutoCString warnMsg;
226 warnMsg.AppendLiteral("Unable to vacuum database: ");
227 warnMsg.Append(mDBFilename);
228 warnMsg.AppendLiteral(" - ");
229 warnMsg.AppendInt(result);
230 warnMsg.Append(' ');
231 warnMsg.Append(message);
232 NS_WARNING(warnMsg.get());
233 #endif
234
235 if (MOZ_LOG_TEST(gStorageLog, LogLevel::Error)) {
236 rv = aError->GetResult(&result);
237 NS_ENSURE_SUCCESS(rv, rv);
238 rv = aError->GetMessage(message);
239 NS_ENSURE_SUCCESS(rv, rv);
240 MOZ_LOG(gStorageLog, LogLevel::Error,
241 ("Vacuum failed with error: %d '%s'. Database was: '%s'", result,
242 message.get(), mDBFilename.get()));
243 }
244 return NS_OK;
245 }
246
247 NS_IMETHODIMP
HandleResult(mozIStorageResultSet * aResultSet)248 Vacuumer::HandleResult(mozIStorageResultSet *aResultSet) {
249 NS_NOTREACHED("Got a resultset from a vacuum?");
250 return NS_OK;
251 }
252
253 NS_IMETHODIMP
HandleCompletion(uint16_t aReason)254 Vacuumer::HandleCompletion(uint16_t aReason) {
255 if (aReason == REASON_FINISHED) {
256 // Update last vacuum time.
257 int32_t now = static_cast<int32_t>(PR_Now() / PR_USEC_PER_SEC);
258 MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty");
259 nsAutoCString prefName(PREF_VACUUM_BRANCH);
260 prefName += mDBFilename;
261 DebugOnly<nsresult> rv = Preferences::SetInt(prefName.get(), now);
262 MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference");
263 }
264
265 notifyCompletion(aReason == REASON_FINISHED);
266
267 return NS_OK;
268 }
269
notifyCompletion(bool aSucceeded)270 nsresult Vacuumer::notifyCompletion(bool aSucceeded) {
271 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
272 if (os) {
273 os->NotifyObservers(nullptr, OBSERVER_TOPIC_HEAVY_IO,
274 OBSERVER_DATA_VACUUM_END);
275 }
276
277 nsresult rv = mParticipant->OnEndVacuum(aSucceeded);
278 NS_ENSURE_SUCCESS(rv, rv);
279
280 return NS_OK;
281 }
282
283 } // namespace
284
285 ////////////////////////////////////////////////////////////////////////////////
286 //// VacuumManager
287
288 NS_IMPL_ISUPPORTS(VacuumManager, nsIObserver)
289
290 VacuumManager *VacuumManager::gVacuumManager = nullptr;
291
getSingleton()292 already_AddRefed<VacuumManager> VacuumManager::getSingleton() {
293 // Don't allocate it in the child Process.
294 if (!XRE_IsParentProcess()) {
295 return nullptr;
296 }
297
298 if (!gVacuumManager) {
299 auto manager = MakeRefPtr<VacuumManager>();
300 MOZ_ASSERT(gVacuumManager == manager.get());
301 return manager.forget();
302 }
303 return do_AddRef(gVacuumManager);
304 }
305
VacuumManager()306 VacuumManager::VacuumManager() : mParticipants("vacuum-participant") {
307 MOZ_ASSERT(!gVacuumManager,
308 "Attempting to create two instances of the service!");
309 gVacuumManager = this;
310 }
311
~VacuumManager()312 VacuumManager::~VacuumManager() {
313 // Remove the static reference to the service. Check to make sure its us
314 // in case somebody creates an extra instance of the service.
315 MOZ_ASSERT(gVacuumManager == this,
316 "Deleting a non-singleton instance of the service");
317 if (gVacuumManager == this) {
318 gVacuumManager = nullptr;
319 }
320 }
321
322 ////////////////////////////////////////////////////////////////////////////////
323 //// nsIObserver
324
325 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)326 VacuumManager::Observe(nsISupports *aSubject, const char *aTopic,
327 const char16_t *aData) {
328 if (strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY) == 0) {
329 // Try to run vacuum on all registered entries. Will stop at the first
330 // successful one.
331 nsCOMArray<mozIStorageVacuumParticipant> entries;
332 mParticipants.GetEntries(entries);
333 // If there are more entries than what a month can contain, we could end up
334 // skipping some, since we run daily. So we use a starting index.
335 static const char *kPrefName = PREF_VACUUM_BRANCH "index";
336 int32_t startIndex = Preferences::GetInt(kPrefName, 0);
337 if (startIndex >= entries.Count()) {
338 startIndex = 0;
339 }
340 int32_t index;
341 for (index = startIndex; index < entries.Count(); ++index) {
342 RefPtr<Vacuumer> vacuum = new Vacuumer(entries[index]);
343 // Only vacuum one database per day.
344 if (vacuum->execute()) {
345 break;
346 }
347 }
348 DebugOnly<nsresult> rv = Preferences::SetInt(kPrefName, index);
349 MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference");
350 }
351
352 return NS_OK;
353 }
354
355 } // namespace storage
356 } // namespace mozilla
357