Bug 411894 - Flush all mozStorageConnections' caches in response to memory pressure events. r=mak
authorGabriele Svelto <gsvelto@mozilla.com>
Thu, 27 Mar 2014 11:19:49 +0100
changeset 175637 e7fb993a5f8a1049ec727c3764e7703e5f0cfd32
parent 175636 53cf2ee44e3d21ccfed38b125bc422c6ea7d3f8c
child 175638 ed98f4616956e84b455000dbe409648c6fb11586
push id26496
push userkwierso@gmail.com
push dateFri, 28 Mar 2014 02:28:34 +0000
treeherdermozilla-central@3c09159e01da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs411894
milestone31.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 411894 - Flush all mozStorageConnections' caches in response to memory pressure events. r=mak
storage/public/mozIStorageAsyncConnection.idl
storage/src/mozStorageConnection.cpp
storage/src/mozStorageService.cpp
storage/src/mozStorageService.h
storage/test/unit/test_connection_executeSimpleSQLAsync.js
storage/test/unit/xpcshell.ini
--- a/storage/public/mozIStorageAsyncConnection.idl
+++ b/storage/public/mozIStorageAsyncConnection.idl
@@ -18,17 +18,17 @@ interface nsIFile;
 
 /**
  * mozIStorageAsyncConnection represents an asynchronous database
  * connection attached to a specific file or to an in-memory data
  * storage.  It is the primary interface for interacting with a
  * database from the main thread, including creating prepared
  * statements, executing SQL, and examining database errors.
  */
-[scriptable, uuid(0e661a1d-27ff-4e6b-ac5a-126314cef61a)]
+[scriptable, uuid(8bfd34d5-4ddf-4e4b-89dd-9b14f33534c6)]
 interface mozIStorageAsyncConnection : nsISupports {
   /**
    * Close this database connection, allowing all pending statements
    * to complete first.
    *
    * @param aCallback [optional]
    *        A callback that will be notified when the close is completed,
    *        with the following arguments:
@@ -124,16 +124,30 @@ interface mozIStorageAsyncConnection : n
    *        re-entrant since they can be called on multiple threads.
    */
   mozIStoragePendingStatement executeAsync(
     [array, size_is(aNumStatements)] in mozIStorageBaseStatement aStatements,
     in unsigned long aNumStatements,
     [optional] in mozIStorageStatementCallback aCallback
   );
 
+  /**
+   * Execute asynchronously an SQL expression, expecting no arguments.
+   *
+   * @param aSQLStatement
+   *        The SQL statement to execute
+   * @param aCallback [optional]
+   *        The callback object that will be notified of progress, errors, and
+   *        completion.
+   * @return an object that can be used to cancel the statement execution.
+   */
+  mozIStoragePendingStatement executeSimpleSQLAsync(
+    in AUTF8String aSQLStatement,
+    [optional] in mozIStorageStatementCallback aCallback);
+
   //////////////////////////////////////////////////////////////////////////////
   //// Functions
 
   /**
    * Create a new SQL function.  If you use your connection on multiple threads,
    * your function needs to be threadsafe, or it should only be called on one
    * thread.
    *
--- a/storage/src/mozStorageConnection.cpp
+++ b/storage/src/mozStorageConnection.cpp
@@ -1383,16 +1383,41 @@ Connection::ExecuteAsync(mozIStorageBase
     NS_ENSURE_TRUE(stmts.AppendElement(data), NS_ERROR_OUT_OF_MEMORY);
   }
 
   // Dispatch to the background
   return AsyncExecuteStatements::execute(stmts, this, aCallback, _handle);
 }
 
 NS_IMETHODIMP
+Connection::ExecuteSimpleSQLAsync(const nsACString &aSQLStatement,
+                                  mozIStorageStatementCallback *aCallback,
+                                  mozIStoragePendingStatement **_handle)
+{
+  if (!NS_IsMainThread()) {
+    return NS_ERROR_NOT_SAME_THREAD;
+  }
+
+  nsCOMPtr<mozIStorageAsyncStatement> stmt;
+  nsresult rv = CreateAsyncStatement(aSQLStatement, getter_AddRefs(stmt));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  nsCOMPtr<mozIStoragePendingStatement> pendingStatement;
+  rv = stmt->ExecuteAsync(aCallback, getter_AddRefs(pendingStatement));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  NS_ADDREF(*_handle = pendingStatement);
+  return rv;
+}
+
+NS_IMETHODIMP
 Connection::TableExists(const nsACString &aTableName,
                         bool *_exists)
 {
     return databaseElementExists(TABLE, aTableName, _exists);
 }
 
 NS_IMETHODIMP
 Connection::IndexExists(const nsACString &aIndexName,
--- a/storage/src/mozStorageService.cpp
+++ b/storage/src/mozStorageService.cpp
@@ -19,16 +19,17 @@
 #include "nsILocaleService.h"
 #include "nsIXPConnect.h"
 #include "nsIObserverService.h"
 #include "nsIPropertyBag2.h"
 #include "mozilla/Services.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/LateWriteChecks.h"
 #include "mozIStorageCompletionCallback.h"
+#include "mozIStoragePendingStatement.h"
 
 #include "sqlite3.h"
 
 #ifdef SQLITE_OS_WIN
 // "windows.h" was included and it can #define lots of things we care about...
 #undef CompareString
 #endif
 
@@ -337,16 +338,44 @@ Service::getConnections(/* inout */ nsTA
 {
   mRegistrationMutex.AssertNotCurrentThreadOwns();
   MutexAutoLock mutex(mRegistrationMutex);
   aConnections.Clear();
   aConnections.AppendElements(mConnections);
 }
 
 void
+Service::minimizeMemory()
+{
+  nsTArray<nsRefPtr<Connection> > connections;
+  getConnections(connections);
+
+  for (uint32_t i = 0; i < connections.Length(); i++) {
+    nsRefPtr<Connection> conn = connections[i];
+    if (conn->ConnectionReady()) {
+      NS_NAMED_LITERAL_CSTRING(shrinkPragma, "PRAGMA shrink_memory");
+      nsCOMPtr<mozIStorageConnection> syncConn = do_QueryInterface(
+        NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, conn));
+      DebugOnly<nsresult> rv;
+
+      if (!syncConn) {
+        nsCOMPtr<mozIStoragePendingStatement> ps;
+        rv = connections[i]->ExecuteSimpleSQLAsync(shrinkPragma, nullptr,
+          getter_AddRefs(ps));
+      } else {
+        rv = connections[i]->ExecuteSimpleSQL(shrinkPragma);
+      }
+
+      MOZ_ASSERT(NS_SUCCEEDED(rv),
+        "Should have been able to purge sqlite caches");
+    }
+  }
+}
+
+void
 Service::shutdown()
 {
   NS_IF_RELEASE(sXPConnect);
 }
 
 sqlite3_vfs *ConstructTelemetryVFS();
 
 #ifdef MOZ_STORAGE_MEMORY
@@ -459,16 +488,22 @@ const sqlite3_mem_methods memMethods = {
   &sqliteMemShutdown,
   nullptr
 };
 
 } // anonymous namespace
 
 #endif  // MOZ_STORAGE_MEMORY
 
+static const char* sObserverTopics[] = {
+  "memory-pressure",
+  "xpcom-shutdown",
+  "xpcom-shutdown-threads"
+};
+
 nsresult
 Service::initialize()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Must be initialized on the main thread");
 
   int rc;
 
 #ifdef MOZ_STORAGE_MEMORY
@@ -492,20 +527,23 @@ Service::initialize()
   } else {
     NS_WARNING("Failed to register telemetry VFS");
   }
 
   // Register for xpcom-shutdown so we can cleanup after ourselves.  The
   // observer service can only be used on the main thread.
   nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
   NS_ENSURE_TRUE(os, NS_ERROR_FAILURE);
-  nsresult rv = os->AddObserver(this, "xpcom-shutdown", false);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = os->AddObserver(this, "xpcom-shutdown-threads", false);
-  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (size_t i = 0; i < ArrayLength(sObserverTopics); ++i) {
+    nsresult rv = os->AddObserver(this, sObserverTopics[i], false);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
 
   // We cache XPConnect for our language helpers.  XPConnect can only be
   // used on the main thread.
   (void)CallGetService(nsIXPConnect::GetCID(), &sXPConnect);
 
   // We need to obtain the toolkit.storage.synchronous preferences on the main
   // thread because the preference service can only be accessed there.  This
   // is cached in the service for all future Open[Unshared]Database calls.
@@ -856,22 +894,28 @@ Service::BackupDatabaseFile(nsIFile *aDB
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
 NS_IMETHODIMP
 Service::Observe(nsISupports *, const char *aTopic, const char16_t *)
 {
-  if (strcmp(aTopic, "xpcom-shutdown") == 0)
+  if (strcmp(aTopic, "memory-pressure") == 0) {
+    minimizeMemory();
+  } else if (strcmp(aTopic, "xpcom-shutdown") == 0) {
     shutdown();
-  if (strcmp(aTopic, "xpcom-shutdown-threads") == 0) {
+  } else if (strcmp(aTopic, "xpcom-shutdown-threads") == 0) {
     nsCOMPtr<nsIObserverService> os =
       mozilla::services::GetObserverService();
-    os->RemoveObserver(this, "xpcom-shutdown-threads");
+
+    for (size_t i = 0; i < ArrayLength(sObserverTopics); ++i) {
+      (void)os->RemoveObserver(this, sObserverTopics[i]);
+    }
+
     bool anyOpen = false;
     do {
       nsTArray<nsRefPtr<Connection> > connections;
       getConnections(connections);
       anyOpen = false;
       for (uint32_t i = 0; i < connections.Length(); i++) {
         nsRefPtr<Connection> &conn = connections[i];
 
--- a/storage/src/mozStorageService.h
+++ b/storage/src/mozStorageService.h
@@ -146,16 +146,22 @@ private:
 
   /**
    * The list of connections we have created.  Modifications to it are
    * protected by |mRegistrationMutex|.
    */
   nsTArray<nsRefPtr<Connection> > mConnections;
 
   /**
+   * Frees as much heap memory as possible from all of the known open
+   * connections.
+   */
+  void minimizeMemory();
+
+  /**
    * Shuts down the storage service, freeing all of the acquired resources.
    */
   void shutdown();
 
   /**
    * Lazily creates and returns a collation created from the application's
    * locale that all statements of all Connections of this Service may use.
    * Since the collation's lifetime is that of the Service and no statement may
new file mode 100644
--- /dev/null
+++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js
@@ -0,0 +1,131 @@
+/* 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/. */
+
+/*
+ * This file tests the functionality of
+ * mozIStorageAsyncConnection::executeSimpleSQLAsync.
+ */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+
+function asyncClose(db) {
+  let deferred = Promise.defer();
+  db.asyncClose(function(status) {
+    if (Components.isSuccessCode(status)) {
+      deferred.resolve();
+    } else {
+      deferred.reject(status);
+    }
+  });
+  return deferred.promise;
+}
+
+function openAsyncDatabase(file) {
+  let deferred = Promise.defer();
+  getService().openAsyncDatabase(file, null, function(status, db) {
+    if (Components.isSuccessCode(status)) {
+      deferred.resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
+    } else {
+      deferred.reject(status);
+    }
+  });
+  return deferred.promise;
+}
+
+function executeSimpleSQLAsync(db, query, onResult) {
+  let deferred = Promise.defer();
+  db.executeSimpleSQLAsync(query, {
+    handleError: function(error) {
+      deferred.reject(error);
+    },
+    handleResult: function(result) {
+      if (onResult) {
+        onResult(result);
+      } else {
+        do_throw("No results were expected");
+      }
+    },
+    handleCompletion: function(result) {
+      deferred.resolve(result);
+    }
+  });
+  return deferred.promise;
+}
+
+add_task(function test_create_and_add() {
+  let adb = yield openAsyncDatabase(getTestDB());
+
+  let completion = yield executeSimpleSQLAsync(adb,
+    "CREATE TABLE test (id INTEGER, string TEXT, number REAL)");
+
+  do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+  completion = yield executeSimpleSQLAsync(adb,
+    "INSERT INTO test (id, string, number) " +
+    "VALUES (" + INTEGER + ", \"" + TEXT + "\", " + REAL + ")");
+
+  do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+  let result = null;
+
+  completion = yield executeSimpleSQLAsync(adb,
+    "SELECT string, number FROM test WHERE id = 1",
+    function(aResultSet) {
+      result = aResultSet.getNextRow();
+      do_check_eq(2, result.numEntries);
+      do_check_eq(TEXT, result.getString(0));
+      do_check_eq(REAL, result.getDouble(1));
+    }
+  );
+
+  do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+  do_check_neq(result, null);
+  result = null;
+
+  yield executeSimpleSQLAsync(adb, "SELECT COUNT(0) FROM test",
+    function(aResultSet) {
+      result = aResultSet.getNextRow();
+      do_check_eq(1, result.getInt32(0));
+    });
+
+  do_check_neq(result, null);
+
+  yield asyncClose(adb);
+});
+
+
+add_task(function test_asyncClose_does_not_complete_before_statement() {
+  let adb = yield openAsyncDatabase(getTestDB());
+  let executed = false;
+
+  let reason = yield executeSimpleSQLAsync(adb, "SELECT * FROM test",
+    function(aResultSet) {
+      let result = aResultSet.getNextRow();
+
+      do_check_neq(result, null);
+      do_check_eq(3, result.numEntries);
+      do_check_eq(INTEGER, result.getInt32(0));
+      do_check_eq(TEXT, result.getString(1));
+      do_check_eq(REAL, result.getDouble(2));
+      executed = true;
+    }
+  );
+
+  do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason);
+
+  // Ensure that the statement executed to completion.
+  do_check_true(executed);
+
+  yield asyncClose(adb);
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Runner
+
+function run_test()
+{
+  run_next_test();
+}
--- a/storage/test/unit/xpcshell.ini
+++ b/storage/test/unit/xpcshell.ini
@@ -12,16 +12,17 @@ support-files =
 [test_bug-393952.js]
 [test_bug-429521.js]
 [test_bug-444233.js]
 [test_cache_size.js]
 [test_chunk_growth.js]
 # Bug 676981: test fails consistently on Android
 fail-if = os == "android"
 [test_connection_executeAsync.js]
+[test_connection_executeSimpleSQLAsync.js]
 [test_js_helpers.js]
 [test_levenshtein.js]
 [test_like.js]
 [test_like_escape.js]
 [test_locale_collation.js]
 [test_page_size_is_32k.js]
 [test_sqlite_secure_delete.js]
 [test_statement_executeAsync.js]