diff --git a/services/datareporting/DataReportingService.js b/services/datareporting/DataReportingService.js index 0bf81b05fad8..0bca5e00e203 100644 --- a/services/datareporting/DataReportingService.js +++ b/services/datareporting/DataReportingService.js @@ -32,7 +32,7 @@ const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000; * EXAMPLE USAGE * ============= * - * let reporter = Cc["@mozilla.org/healthreport/service;1"] + * let reporter = Cc["@mozilla.org/datareporting/service;1"] * .getService(Ci.nsISupports) * .wrappedJSObject * .healthReporter; diff --git a/services/healthreport/healthreporter.jsm b/services/healthreport/healthreporter.jsm index ebb61280920a..482d08f4e487 100644 --- a/services/healthreport/healthreporter.jsm +++ b/services/healthreport/healthreporter.jsm @@ -38,7 +38,8 @@ const DEFAULT_DATABASE_NAME = "healthreport.sqlite"; * lower-level components (such as collection and submission) together. * * An instance of this type is created as an XPCOM service. See - * HealthReportService.js and HealthReportComponents.manifest. + * DataReportingService.js and + * DataReporting.manifest/HealthReportComponents.manifest. * * It is theoretically possible to have multiple instances of this running * in the application. For example, this type may one day handle submission @@ -306,6 +307,9 @@ HealthReporter.prototype = Object.freeze({ this._log.info("HealthReporter started."); this._initialized = true; Services.obs.addObserver(this, "idle-daily", false); + + // Clean up caches and reduce memory usage. + this._storage.compact(); this._initializedDeferred.resolve(this); }, @@ -483,7 +487,7 @@ HealthReporter.prototype = Object.freeze({ * Register a `Metrics.Provider` with this instance. * * This needs to be called or no data will be collected. See also - * registerProvidersFromCategoryManager`. + * `registerProvidersFromCategoryManager`. * * @param provider * (Metrics.Provider) The provider to register for collection. @@ -626,10 +630,12 @@ HealthReporter.prototype = Object.freeze({ }; for (let [measurementKey, measurement] of provider.measurements) { - let name = providerName + "." + measurement.name + "." + measurement.version; + let name = providerName + "." + measurement.name; let serializer; try { + // The measurement is responsible for returning a serializer which + // is aware of the measurement version. serializer = measurement.serializer(measurement.SERIALIZE_JSON); } catch (ex) { this._log.warn("Error obtaining serializer for measurement: " + name + @@ -692,6 +698,7 @@ HealthReporter.prototype = Object.freeze({ o.errors = errors; } + this._storage.compact(); throw new Task.Result(JSON.stringify(o)); }, diff --git a/services/healthreport/providers.jsm b/services/healthreport/providers.jsm index 10dfeb9207ea..4242fc35469d 100644 --- a/services/healthreport/providers.jsm +++ b/services/healthreport/providers.jsm @@ -394,7 +394,7 @@ CurrentSessionMeasurement.prototype = Object.freeze({ }, _serializeJSONSingular: function (data) { - let result = {}; + let result = {"_v": this.version}; for (let [field, value] of data) { result[field] = value[1]; @@ -535,7 +535,9 @@ ActiveAddonsMeasurement.prototype = Object.freeze({ } // Exceptions are caught in the caller. - return JSON.parse(data.get("addons")[1]); + let result = JSON.parse(data.get("addons")[1]); + result._v = this.version; + return result; }, }); diff --git a/services/healthreport/tests/xpcshell/test_healthreporter.js b/services/healthreport/tests/xpcshell/test_healthreporter.js index fdabf1132430..76d0d1135295 100644 --- a/services/healthreport/tests/xpcshell/test_healthreporter.js +++ b/services/healthreport/tests/xpcshell/test_healthreporter.js @@ -204,8 +204,10 @@ add_task(function test_json_payload_dummy_provider() { print(payload); let o = JSON.parse(payload); + let name = "DummyProvider.DummyMeasurement"; do_check_eq(Object.keys(o.data.last).length, 1); - do_check_true("DummyProvider.DummyMeasurement.1" in o.data.last); + do_check_true(name in o.data.last); + do_check_eq(o.data.last[name]._v, 1); reporter._shutdown(); }); diff --git a/services/healthreport/tests/xpcshell/test_provider_addons.js b/services/healthreport/tests/xpcshell/test_provider_addons.js index cee1fd2c91e8..b2d8e482b982 100644 --- a/services/healthreport/tests/xpcshell/test_provider_addons.js +++ b/services/healthreport/tests/xpcshell/test_provider_addons.js @@ -111,9 +111,10 @@ add_task(function test_collect() { let serializer = active.serializer(active.SERIALIZE_JSON); let serialized = serializer.singular(data.singular); do_check_eq(typeof(serialized), "object"); - do_check_eq(Object.keys(serialized).length, 2); + do_check_eq(Object.keys(serialized).length, 3); // Our two keys, plus _v. do_check_true("addon0" in serialized); do_check_true("addon1" in serialized); + do_check_eq(serialized._v, 1); let counts = provider.getMeasurement("counts", 1); data = yield counts.getValues(); diff --git a/services/healthreport/tests/xpcshell/test_provider_appinfo.js b/services/healthreport/tests/xpcshell/test_provider_appinfo.js index a2804c07aad3..06925c8534dc 100644 --- a/services/healthreport/tests/xpcshell/test_provider_appinfo.js +++ b/services/healthreport/tests/xpcshell/test_provider_appinfo.js @@ -34,6 +34,7 @@ add_task(function test_collect_smoketest() { let serializer = m.serializer(m.SERIALIZE_JSON); let d = serializer.singular(data.singular); + do_check_eq(d._v, 1); do_check_eq(d.vendor, "Mozilla"); do_check_eq(d.name, "xpcshell"); do_check_eq(d.id, "xpcshell@tests.mozilla.org"); diff --git a/services/healthreport/tests/xpcshell/test_provider_sessions.js b/services/healthreport/tests/xpcshell/test_provider_sessions.js index 8c867115f3a1..f3cc5ec1dd4a 100644 --- a/services/healthreport/tests/xpcshell/test_provider_sessions.js +++ b/services/healthreport/tests/xpcshell/test_provider_sessions.js @@ -170,6 +170,7 @@ add_task(function test_serialization() { let serializer = current.serializer(current.SERIALIZE_JSON); let fields = serializer.singular(data.singular); + do_check_eq(fields._v, 2); do_check_eq(fields.activeTicks, 0); do_check_eq(fields.startDay, Metrics.dateToDays(recorder.startDate)); do_check_eq(fields.main, 500); diff --git a/services/healthreport/tests/xpcshell/test_provider_sysinfo.js b/services/healthreport/tests/xpcshell/test_provider_sysinfo.js index db01fc5d19b5..537e5e653668 100644 --- a/services/healthreport/tests/xpcshell/test_provider_sysinfo.js +++ b/services/healthreport/tests/xpcshell/test_provider_sysinfo.js @@ -32,6 +32,7 @@ add_task(function test_collect_smoketest() { let serializer = m.serializer(m.SERIALIZE_JSON); let d = serializer.singular(data.singular); + do_check_eq(d._v, 1); do_check_true(d.cpuCount > 0); do_check_neq(d.name, null); diff --git a/services/metrics/dataprovider.jsm b/services/metrics/dataprovider.jsm index de190d071d8e..ab485d814c05 100644 --- a/services/metrics/dataprovider.jsm +++ b/services/metrics/dataprovider.jsm @@ -246,7 +246,7 @@ Measurement.prototype = Object.freeze({ }, _serializeJSONSingular: function (data) { - let result = {}; + let result = {"_v": this.version}; for (let [field, data] of data) { // There could be legacy fields in storage we no longer care about. @@ -278,7 +278,7 @@ Measurement.prototype = Object.freeze({ }, _serializeJSONDay: function (data) { - let result = {}; + let result = {"_v": this.version}; for (let [field, data] of data) { if (!this._fieldsByName.has(field)) { diff --git a/services/metrics/storage.jsm b/services/metrics/storage.jsm index 1f590b229966..7dd432fa4a44 100644 --- a/services/metrics/storage.jsm +++ b/services/metrics/storage.jsm @@ -1204,6 +1204,21 @@ MetricsStorageSqliteBackend.prototype = Object.freeze({ }); }, + /** + * Reduce memory usage as much as possible. + * + * This returns a promise that will be resolved on completion. + * + * @return Promise<> + */ + compact: function () { + let self = this; + return this.enqueueOperation(function doCompact() { + self._connection.discardCachedStatements(); + return self._connection.shrinkMemory(); + }); + }, + /** * Ensure a field ID matches a specified type. * diff --git a/services/metrics/tests/xpcshell/test_metrics_provider.js b/services/metrics/tests/xpcshell/test_metrics_provider.js index 5042a6244716..15590d7504c0 100644 --- a/services/metrics/tests/xpcshell/test_metrics_provider.js +++ b/services/metrics/tests/xpcshell/test_metrics_provider.js @@ -256,15 +256,17 @@ add_task(function test_serialize_json_default() { let serializer = m.serializer(m.SERIALIZE_JSON); let formatted = serializer.singular(data.singular); - do_check_eq(Object.keys(formatted).length, 2); + do_check_eq(Object.keys(formatted).length, 3); // Our keys + _v. do_check_true("last-numeric" in formatted); do_check_true("last-text" in formatted); do_check_eq(formatted["last-numeric"], 6); do_check_eq(formatted["last-text"], "hello"); + do_check_eq(formatted["_v"], 1); formatted = serializer.daily(data.days.getDay(now)); - do_check_eq(Object.keys(formatted).length, 5); + do_check_eq(Object.keys(formatted).length, 6); // Our keys + _v. do_check_eq(formatted["daily-counter"], 2); + do_check_eq(formatted["_v"], 1); do_check_true(Array.isArray(formatted["daily-discrete-numeric"])); do_check_eq(formatted["daily-discrete-numeric"].length, 2); diff --git a/toolkit/modules/Sqlite.jsm b/toolkit/modules/Sqlite.jsm index ead715b1a1b3..a487c3e39d94 100644 --- a/toolkit/modules/Sqlite.jsm +++ b/toolkit/modules/Sqlite.jsm @@ -8,7 +8,7 @@ this.EXPORTED_SYMBOLS = [ "Sqlite", ]; -const {interfaces: Ci, utils: Cu} = Components; +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/commonjs/promise/core.js"); Cu.import("resource://gre/modules/osfile.jsm"); @@ -43,6 +43,13 @@ let connectionCounters = {}; * to obtain a lock, possibly making database access slower. Defaults to * true. * + * shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection + * will attempt to minimize its memory usage after this many + * milliseconds of connection idle. The connection is idle when no + * statements are executing. There is no default value which means no + * automatic memory minimization will occur. Please note that this is + * *not* a timer on the idle service and this could fire while the + * application is active. * * FUTURE options to control: * @@ -69,6 +76,18 @@ function openConnection(options) { let sharedMemoryCache = "sharedMemoryCache" in options ? options.sharedMemoryCache : true; + let openedOptions = {}; + + if ("shrinkMemoryOnConnectionIdleMS" in options) { + if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) { + throw new Error("shrinkMemoryOnConnectionIdleMS must be an integer. " + + "Got: " + options.shrinkMemoryOnConnectionIdleMS); + } + + openedOptions.shrinkMemoryOnConnectionIdleMS = + options.shrinkMemoryOnConnectionIdleMS; + } + let file = FileUtils.File(path); let openDatabaseFn = sharedMemoryCache ? Services.storage.openDatabase : @@ -92,7 +111,8 @@ function openConnection(options) { return Promise.reject(new Error("Connection is not ready.")); } - return Promise.resolve(new OpenedConnection(connection, basename, number)); + return Promise.resolve(new OpenedConnection(connection, basename, number, + openedOptions)); } catch (ex) { log.warn("Could not open database: " + CommonUtils.exceptionStr(ex)); return Promise.reject(ex); @@ -143,8 +163,11 @@ function openConnection(options) { * (string) The basename of this database name. Used for logging. * @param number * (Number) The connection number to this database. + * @param options + * (object) Options to control behavior of connection. See + * `openConnection`. */ -function OpenedConnection(connection, basename, number) { +function OpenedConnection(connection, basename, number, options) { let log = Log4Moz.repository.getLogger("Sqlite.Connection." + basename); // getLogger() returns a shared object. We can't modify the functions on this @@ -176,10 +199,23 @@ function OpenedConnection(connection, basename, number) { this._cachedStatements = new Map(); this._anonymousStatements = new Map(); this._anonymousCounter = 0; - this._inProgressStatements = new Map(); - this._inProgressCounter = 0; + + // A map from statement index to mozIStoragePendingStatement, to allow for + // canceling prior to finalizing the mozIStorageStatements. + this._pendingStatements = new Map(); + + // Increments for each executed statement for the life of the connection. + this._statementCounter = 0; this._inProgressTransaction = null; + + this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS; + if (this._idleShrinkMS) { + this._idleShrinkTimer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + // We wait for the first statement execute to start the timer because + // shrinking now would not do anything. + } } OpenedConnection.prototype = Object.freeze({ @@ -259,7 +295,7 @@ OpenedConnection.prototype = Object.freeze({ } this._log.debug("Request to close connection."); - + this._clearIdleShrinkTimer(); let deferred = Promise.defer(); // We need to take extra care with transactions during shutdown. @@ -287,11 +323,14 @@ OpenedConnection.prototype = Object.freeze({ _finalize: function (deferred) { this._log.debug("Finalizing connection."); - // Cancel any in-progress statements. - for (let [k, statement] of this._inProgressStatements) { + // Cancel any pending statements. + for (let [k, statement] of this._pendingStatements) { statement.cancel(); } - this._inProgressStatements.clear(); + this._pendingStatements.clear(); + + // We no longer need to track these. + this._statementCounter = 0; // Next we finalize all active statements. for (let [k, statement] of this._anonymousStatements) { @@ -389,7 +428,27 @@ OpenedConnection.prototype = Object.freeze({ this._cachedStatements.set(sql, statement); } - return this._executeStatement(sql, statement, params, onRow); + this._clearIdleShrinkTimer(); + + let deferred = Promise.defer(); + + try { + this._executeStatement(sql, statement, params, onRow).then( + function onResult(result) { + this._startIdleShrinkTimer(); + deferred.resolve(result); + }.bind(this), + function onError(error) { + this._startIdleShrinkTimer(); + deferred.reject(error); + }.bind(this) + ); + } catch (ex) { + this._startIdleShrinkTimer(); + throw ex; + } + + return deferred.promise; }, /** @@ -418,22 +477,32 @@ OpenedConnection.prototype = Object.freeze({ let index = this._anonymousCounter++; this._anonymousStatements.set(index, statement); + this._clearIdleShrinkTimer(); + + let onFinished = function () { + this._anonymousStatements.delete(index); + statement.finalize(); + this._startIdleShrinkTimer(); + }.bind(this); let deferred = Promise.defer(); - this._executeStatement(sql, statement, params, onRow).then( - function onResult(rows) { - this._anonymousStatements.delete(index); - statement.finalize(); - deferred.resolve(rows); - }.bind(this), + try { + this._executeStatement(sql, statement, params, onRow).then( + function onResult(rows) { + onFinished(); + deferred.resolve(rows); + }.bind(this), - function onError(error) { - this._anonymousStatements.delete(index); - statement.finalize(); - deferred.reject(error); - }.bind(this) - ); + function onError(error) { + onFinished(); + deferred.reject(error); + }.bind(this) + ); + } catch (ex) { + onFinished(); + throw ex; + } return deferred.promise; }, @@ -586,6 +655,40 @@ OpenedConnection.prototype = Object.freeze({ ); }, + /** + * Free up as much memory from the underlying database connection as possible. + * + * @return Promise<> + */ + shrinkMemory: function () { + this._log.info("Shrinking memory usage."); + + let onShrunk = this._clearIdleShrinkTimer.bind(this); + + return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk); + }, + + /** + * Discard all cached statements. + * + * Note that this relies on us being non-interruptible between + * the insertion or retrieval of a statement in the cache and its + * execution: we finalize all statements, which is only safe if + * they will not be executed again. + * + * @return (integer) the number of statements discarded. + */ + discardCachedStatements: function () { + let count = 0; + for (let [k, statement] of this._cachedStatements) { + ++count; + statement.finalize(); + } + this._cachedStatements.clear(); + this._log.debug("Discarded " + count + " cached statements."); + return count; + }, + _executeStatement: function (sql, statement, params, onRow) { if (statement.state != statement.MOZ_STORAGE_STATEMENT_READY) { throw new Error("Statement is not ready for execution."); @@ -608,7 +711,7 @@ OpenedConnection.prototype = Object.freeze({ "object. Got: " + params); } - let index = this._inProgressCounter++; + let index = this._statementCounter++; let deferred = Promise.defer(); let userCancelled = false; @@ -659,8 +762,8 @@ OpenedConnection.prototype = Object.freeze({ }, handleCompletion: function (reason) { - self._log.debug("Stmt #" + index + " finished"); - self._inProgressStatements.delete(index); + self._log.debug("Stmt #" + index + " finished."); + self._pendingStatements.delete(index); switch (reason) { case Ci.mozIStorageStatementCallback.REASON_FINISHED: @@ -695,8 +798,7 @@ OpenedConnection.prototype = Object.freeze({ }, }); - this._inProgressStatements.set(index, pending); - + this._pendingStatements.set(index, pending); return deferred.promise; }, @@ -705,6 +807,24 @@ OpenedConnection.prototype = Object.freeze({ throw new Error("Connection is not open."); } }, + + _clearIdleShrinkTimer: function () { + if (!this._idleShrinkTimer) { + return; + } + + this._idleShrinkTimer.cancel(); + }, + + _startIdleShrinkTimer: function () { + if (!this._idleShrinkTimer) { + return; + } + + this._idleShrinkTimer.initWithCallback(this.shrinkMemory.bind(this), + this._idleShrinkMS, + this._idleShrinkTimer.TYPE_ONE_SHOT); + }, }); this.Sqlite = { diff --git a/toolkit/modules/tests/xpcshell/test_sqlite.js b/toolkit/modules/tests/xpcshell/test_sqlite.js index 1f4455332d31..aed7e966475d 100644 --- a/toolkit/modules/tests/xpcshell/test_sqlite.js +++ b/toolkit/modules/tests/xpcshell/test_sqlite.js @@ -3,7 +3,7 @@ "use strict"; -const {utils: Cu} = Components; +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; do_get_profile(); @@ -12,23 +12,46 @@ Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Sqlite.jsm"); Cu.import("resource://gre/modules/Task.jsm"); +// To spin the event loop in test. +Cu.import("resource://services-common/async.js"); -function getConnection(dbName) { - let path = dbName + ".sqlite"; +function sleep(ms) { + let deferred = Promise.defer(); - return Sqlite.openConnection({path: path}); + let timer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + + timer.initWithCallback({ + notify: function () { + deferred.resolve(); + }, + }, ms, timer.TYPE_ONE_SHOT); + + return deferred.promise; } -function getDummyDatabase(name) { +function getConnection(dbName, extraOptions={}) { + let path = dbName + ".sqlite"; + let options = {path: path}; + for (let [k, v] in Iterator(extraOptions)) { + options[k] = v; + } + + return Sqlite.openConnection(options); +} + +function getDummyDatabase(name, extraOptions={}) { const TABLES = { dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", }; - let c = yield getConnection(name); + let c = yield getConnection(name, extraOptions); + c._initialStatementCount = 0; for (let [k, v] in Iterator(TABLES)) { yield c.execute("CREATE TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; } throw new Task.Result(c); @@ -161,11 +184,17 @@ add_task(function test_execute_invalid_statement() { let deferred = Promise.defer(); + do_check_eq(c._anonymousStatements.size, 0); + c.execute("SELECT invalid FROM unknown").then(do_throw, function onError(error) { deferred.resolve(); }); yield deferred.promise; + + // Ensure we don't leak the statement instance. + do_check_eq(c._anonymousStatements.size, 0); + yield c.close(); }); @@ -323,3 +352,223 @@ add_task(function test_detect_multiple_transactions() { yield c.close(); }); +add_task(function test_shrink_memory() { + let c = yield getDummyDatabase("shrink_memory"); + + // It's just a simple sanity test. We have no way of measuring whether this + // actually does anything. + + yield c.shrinkMemory(); + yield c.close(); +}); + +add_task(function test_no_shrink_on_init() { + let c = yield getConnection("no_shrink_on_init", + {shrinkMemoryOnConnectionIdleMS: 200}); + + let oldShrink = c.shrinkMemory; + let count = 0; + Object.defineProperty(c, "shrinkMemory", { + value: function () { + count++; + }, + }); + + // We should not shrink until a statement has been executed. + yield sleep(220); + do_check_eq(count, 0); + + yield c.execute("SELECT 1"); + yield sleep(220); + do_check_eq(count, 1); + + yield c.close(); +}); + +add_task(function test_idle_shrink_fires() { + let c = yield getDummyDatabase("idle_shrink_fires", + {shrinkMemoryOnConnectionIdleMS: 200}); + c._clearIdleShrinkTimer(); + + let oldShrink = c.shrinkMemory; + let shrinkPromises = []; + + let count = 0; + Object.defineProperty(c, "shrinkMemory", { + value: function () { + count++; + let promise = oldShrink.call(c); + shrinkPromises.push(promise); + return promise; + }, + }); + + // We reset the idle shrink timer after monkeypatching because otherwise the + // installed timer callback will reference the non-monkeypatched function. + c._startIdleShrinkTimer(); + + yield sleep(220); + do_check_eq(count, 1); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + shrinkPromises.shift(); + + // We shouldn't shrink again unless a statement was executed. + yield sleep(300); + do_check_eq(count, 1); + + yield c.execute("SELECT 1"); + yield sleep(300); + + do_check_eq(count, 2); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + + yield c.close(); +}); + +add_task(function test_idle_shrink_reset_on_operation() { + const INTERVAL = 500; + let c = yield getDummyDatabase("idle_shrink_reset_on_operation", + {shrinkMemoryOnConnectionIdleMS: INTERVAL}); + + c._clearIdleShrinkTimer(); + + let oldShrink = c.shrinkMemory; + let shrinkPromises = []; + let count = 0; + + Object.defineProperty(c, "shrinkMemory", { + value: function () { + count++; + let promise = oldShrink.call(c); + shrinkPromises.push(promise); + return promise; + }, + }); + + let now = new Date(); + c._startIdleShrinkTimer(); + + let initialIdle = new Date(now.getTime() + INTERVAL); + + // Perform database operations until initial scheduled time has been passed. + let i = 0; + while (new Date() < initialIdle) { + yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["" + i]); + i++; + } + + do_check_true(i > 0); + + // We should not have performed an idle while doing operations. + do_check_eq(count, 0); + + // Wait for idle timer. + yield sleep(INTERVAL); + + // Ensure we fired. + do_check_eq(count, 1); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + + yield c.close(); +}); + +add_task(function test_in_progress_counts() { + let c = yield getDummyDatabase("in_progress_counts"); + do_check_eq(c._statementCounter, c._initialStatementCount); + do_check_eq(c._pendingStatements.size, 0); + yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + do_check_eq(c._statementCounter, c._initialStatementCount + 1); + do_check_eq(c._pendingStatements.size, 0); + + let expectOne; + let expectTwo; + + // Please forgive me. + let inner = Async.makeSpinningCallback(); + let outer = Async.makeSpinningCallback(); + + // We want to make sure that two queries executing simultaneously + // result in `_pendingStatements.size` reaching 2, then dropping back to 0. + // + // To do so, we kick off a second statement within the row handler + // of the first, then wait for both to finish. + + yield c.executeCached("SELECT * from dirs", null, function onRow() { + // In the onRow handler, we're still an outstanding query. + // Expect a single in-progress entry. + expectOne = c._pendingStatements.size; + + // Start another query, checking that after its statement has been created + // there are two statements in progress. + let p = c.executeCached("SELECT 10, path from dirs"); + expectTwo = c._pendingStatements.size; + + // Now wait for it to be done before we return from the row handler … + p.then(function onInner() { + inner(); + }); + }).then(function onOuter() { + // … and wait for the inner to be done before we finish … + inner.wait(); + outer(); + }); + + // … and wait for both queries to have finished before we go on and + // test postconditions. + outer.wait(); + + do_check_eq(expectOne, 1); + do_check_eq(expectTwo, 2); + do_check_eq(c._statementCounter, c._initialStatementCount + 3); + do_check_eq(c._pendingStatements.size, 0); + + yield c.close(); +}); + +add_task(function test_discard_while_active() { + let c = yield getDummyDatabase("discard_while_active"); + + yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + yield c.executeCached("INSERT INTO dirs (path) VALUES ('bar')"); + + let discarded = -1; + let first = true; + let sql = "SELECT * FROM dirs"; + yield c.executeCached(sql, null, function onRow(row) { + if (!first) { + return; + } + first = false; + discarded = c.discardCachedStatements(); + }); + + // We discarded everything, because the SELECT had already started to run. + do_check_eq(3, discarded); + + // And again is safe. + do_check_eq(0, c.discardCachedStatements()); + + yield c.close(); +}); + +add_task(function test_discard_cached() { + let c = yield getDummyDatabase("discard_cached"); + + yield c.executeCached("SELECT * from dirs"); + do_check_eq(1, c._cachedStatements.size); + + yield c.executeCached("SELECT * from files"); + do_check_eq(2, c._cachedStatements.size); + + yield c.executeCached("SELECT * from dirs"); + do_check_eq(2, c._cachedStatements.size); + + c.discardCachedStatements(); + do_check_eq(0, c._cachedStatements.size); + + yield c.close(); +}); +