Bug 1123384 - Move Telemetry main ping implementation out of TelemetryPing.jsm. r=gfritzsche

This commit is contained in:
Alessio Placitelli 2015-01-22 12:23:00 +01:00
parent 849e905c83
commit 557fc749e6
17 changed files with 1344 additions and 1101 deletions

View File

@ -26,8 +26,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryPing",
"resource://gre/modules/TelemetryPing.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySession",
"resource://gre/modules/TelemetrySession.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
"resource://gre/modules/TelemetryLog.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
@ -323,7 +323,7 @@ Experiments.Policy.prototype = {
},
telemetryPayload: function () {
return TelemetryPing.getPayload();
return TelemetrySession.getPayload();
},
/**

View File

@ -5,6 +5,7 @@
Cu.import("resource:///modules/experiments/Experiments.jsm");
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
const FILE_MANIFEST = "experiments.manifest";
const SEC_IN_ONE_DAY = 24 * 60 * 60;
@ -51,6 +52,7 @@ function run_test() {
add_task(function* test_setup() {
createAppInfo();
gProfileDir = do_get_profile();
yield TelemetrySession.setup();
gPolicy = new Experiments.Policy();
gReporter = yield getReporter("json_payload_simple");
@ -308,3 +310,7 @@ add_task(function* test_times() {
}
}
});
add_task(function* test_shutdown() {
yield TelemetrySession.shutdown();
});

View File

@ -103,8 +103,8 @@ add_task(function* test_setup() {
// Test basic starting and stopping of experiments.
add_task(function* test_telemetryBasics() {
// Check TelemetryLog instead of TelemetryPing.getPayload().log because
// TelemetryPing gets Experiments.instance() and side-effects log entries.
// Check TelemetryLog instead of TelemetrySession.getPayload().log because
// TelemetrySession gets Experiments.instance() and side-effects log entries.
const OBSERVER_TOPIC = "experiments-changed";
let observerFireCount = 0;

View File

@ -5,15 +5,21 @@
"use strict";
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
function test() {
runTests();
}
function getTelemetryPayload() {
return Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).
TelemetryPing.getPayload();
return TelemetrySession.getPayload();
}
gTests.push({
desc: "Setup",
run: () => { yield TelemetrySession.setup(); }
});
gTests.push({
desc: "Test browser-ui telemetry",
run: function testBrowserUITelemetry() {
@ -63,4 +69,9 @@ gTests.push({
is(simpleMeasurements.UITelemetry["metro-tabs"]["currTabCount"], 1);
is(simpleMeasurements.UITelemetry["metro-tabs"]["maxTabCount"], 3);
}
});
});
gTests.push({
desc: "Shutdown",
run: () => { yield TelemetrySession.shutdown(); }
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
const Cu = Components.utils;
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
/**
@ -22,6 +23,7 @@ TelemetryStartup.prototype.QueryInterface = XPCOMUtils.generateQI([Components.in
TelemetryStartup.prototype.observe = function(aSubject, aTopic, aData) {
if (aTopic == "profile-after-change" || aTopic == "app-startup") {
TelemetryPing.observe(null, aTopic, null);
TelemetrySession.observe(null, aTopic, null);
}
}

View File

@ -37,6 +37,7 @@ EXTRA_JS_MODULES += [
EXTRA_PP_JS_MODULES += [
'TelemetryPing.jsm',
'TelemetrySession.jsm',
]
FAIL_ON_WARNINGS = True

View File

@ -3,7 +3,7 @@ const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/TelemetryLog.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
const TEST_PREFIX = "TEST-";
const TEST_REGEX = new RegExp("^" + TEST_PREFIX);
@ -27,11 +27,13 @@ function check_event(event, id, data)
function run_test()
{
yield TelemetrySession.setup();
TelemetryLog.log(TEST_PREFIX + "1", ["val", 123, undefined]);
TelemetryLog.log(TEST_PREFIX + "2", []);
TelemetryLog.log(TEST_PREFIX + "3");
var log = TelemetryPing.getPayload().log.filter(function(e) {
var log = TelemetrySession.getPayload().log.filter(function(e) {
// Only want events that were generated by the test.
return TEST_REGEX.test(e[0]);
});
@ -42,4 +44,6 @@ function run_test()
check_event(log[2], TEST_PREFIX + "3", undefined);
do_check_true(log[0][1] <= log[1][1]);
do_check_true(log[1][1] <= log[2][1]);
yield TelemetrySession.shutdown();
}

View File

@ -18,6 +18,7 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
Cu.import("resource://gre/modules/TelemetryFile.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
@ -65,11 +66,13 @@ XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
.wrappedJSObject);
function sendPing () {
TelemetryPing.gatherStartup();
TelemetrySession.gatherStartup();
if (gServerStarted) {
return TelemetryPing.testPing("http://localhost:" + gHttpServer.identity.primaryPort);
TelemetryPing.setServer("http://localhost:" + gHttpServer.identity.primaryPort);
return TelemetrySession.testPing();
} else {
return TelemetryPing.testPing("http://doesnotexist");
TelemetryPing.setServer("http://doesnotexist");
return TelemetrySession.testPing();
}
}
@ -502,6 +505,7 @@ function actualTest() {
}
add_task(function* asyncSetup() {
yield TelemetrySession.setup();
yield TelemetryPing.setup();
if (HAS_DATAREPORTINGSERVICE) {
@ -510,7 +514,7 @@ add_task(function* asyncSetup() {
}
// When no DRS or no DRS.getSessionRecorder(), activeTicks should be -1.
do_check_eq(TelemetryPing.getPayload().simpleMeasurements.activeTicks, -1);
do_check_eq(TelemetrySession.getPayload().simpleMeasurements.activeTicks, -1);
if (HAS_DATAREPORTINGSERVICE) {
// Restore normal behavior for getSessionRecorder()
@ -541,8 +545,8 @@ add_task(function* test_expiredHistogram() {
dummy.add(1);
do_check_eq(TelemetryPing.getPayload()["histograms"][histogram_id], undefined);
do_check_eq(TelemetryPing.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
do_check_eq(TelemetrySession.getPayload()["histograms"][histogram_id], undefined);
do_check_eq(TelemetrySession.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
});
// Checks that an invalid histogram file is deleted if TelemetryFile fails to parse it.
@ -552,7 +556,7 @@ add_task(function* test_runInvalidJSON() {
writeStringToFile(histogramsFile, "this.is.invalid.JSON");
do_check_true(histogramsFile.exists());
yield TelemetryPing.testLoadHistograms(histogramsFile);
yield TelemetrySession.testLoadHistograms(histogramsFile);
do_check_false(histogramsFile.exists());
});
@ -581,21 +585,25 @@ add_task(function* test_saveLoadPing() {
let histogramsFile = getSavedHistogramsFile("saved-histograms.dat");
setupTestData();
yield TelemetryPing.testSaveHistograms(histogramsFile);
yield TelemetryPing.testLoadHistograms(histogramsFile);
yield TelemetrySession.testSaveHistograms(histogramsFile);
yield TelemetrySession.testLoadHistograms(histogramsFile);
yield sendPing();
// Get requests received by dummy server.
let request1 = yield gRequestIterator.next();
let request2 = yield gRequestIterator.next();
// We decode both requests to check for the |reason|.
let payload1 = decodeRequestPayload(request1);
let payload2 = decodeRequestPayload(request2);
// Check we have the correct two requests. Ordering is not guaranteed.
if (request1.path.contains("test-ping")) {
checkPayload(request1, "test-ping", 1);
checkPayload(request2, "saved-session", 1);
if (payload1.info.reason === "test-ping") {
checkPayloadInfo(payload1, "test-ping");
checkPayloadInfo(payload2, "saved-session");
} else {
checkPayload(request1, "saved-session", 1);
checkPayload(request2, "test-ping", 1);
checkPayloadInfo(payload1, "saved-session");
checkPayloadInfo(payload2, "test-ping");
}
});
@ -603,22 +611,22 @@ add_task(function* test_saveLoadPing() {
add_task(function* test_runOldPingFile() {
let histogramsFile = getSavedHistogramsFile("old-histograms.dat");
yield TelemetryPing.testSaveHistograms(histogramsFile);
yield TelemetrySession.testSaveHistograms(histogramsFile);
do_check_true(histogramsFile.exists());
let mtime = histogramsFile.lastModifiedTime;
histogramsFile.lastModifiedTime = mtime - (14 * 24 * 60 * 60 * 1000 + 60000); // 14 days, 1m
yield TelemetryPing.testLoadHistograms(histogramsFile);
yield TelemetrySession.testLoadHistograms(histogramsFile);
do_check_false(histogramsFile.exists());
});
add_task(function* test_savedSessionClientID() {
// Assure that we store the ping properly when saving sessions on shutdown.
// We make the TelemetryPings shutdown to trigger a session save.
// We make the TelemetrySession shutdown to trigger a session save.
const dir = TelemetryFile.pingDirectoryPath;
yield OS.File.removeDir(dir, {ignoreAbsent: true});
yield OS.File.makeDir(dir);
yield TelemetryPing.shutdown();
yield TelemetrySession.shutdown();
yield TelemetryFile.loadSavedPings();
Assert.equal(TelemetryFile.pingsLoaded, 1);

View File

@ -18,7 +18,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
@ -26,8 +26,8 @@ XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
.getService(Ci.nsISupports)
.wrappedJSObject);
// Force the Telemetry enabled preference so that TelemetryPing.reset() doesn't exit early.
Services.prefs.setBoolPref(TelemetryPing.Constants.PREF_ENABLED, true);
// Force the Telemetry enabled preference so that TelemetrySession.reset() doesn't exit early.
Services.prefs.setBoolPref(TelemetrySession.Constants.PREF_ENABLED, true);
// Set up our dummy AppInfo object so we can control the appBuildID.
Cu.import("resource://testing-common/AppInfo.jsm", this);
@ -36,19 +36,19 @@ updateAppInfo();
// Check that when run with no previous build ID stored, we update the pref but do not
// put anything into the metadata.
add_task(function* test_firstRun() {
yield TelemetryPing.reset();
let metadata = TelemetryPing.getMetadata();
yield TelemetrySession.reset();
let metadata = TelemetrySession.getMetadata();
do_check_false("previousBuildID" in metadata);
let appBuildID = getAppInfo().appBuildID;
let buildIDPref = Services.prefs.getCharPref(TelemetryPing.Constants.PREF_PREVIOUS_BUILDID);
let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID);
do_check_eq(appBuildID, buildIDPref);
});
// Check that a subsequent run with the same build ID does not put prev build ID in
// metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref.
add_task(function* test_secondRun() {
yield TelemetryPing.reset();
let metadata = TelemetryPing.getMetadata();
yield TelemetrySession.reset();
let metadata = TelemetrySession.getMetadata();
do_check_false("previousBuildID" in metadata);
});
@ -60,10 +60,10 @@ add_task(function* test_newBuild() {
let info = getAppInfo();
let oldBuildID = info.appBuildID;
info.appBuildID = NEW_BUILD_ID;
yield TelemetryPing.reset();
let metadata = TelemetryPing.getMetadata();
yield TelemetrySession.reset();
let metadata = TelemetrySession.getMetadata();
do_check_eq(metadata.previousBuildID, oldBuildID);
let buildIDPref = Services.prefs.getCharPref(TelemetryPing.Constants.PREF_PREVIOUS_BUILDID);
let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID);
do_check_eq(NEW_BUILD_ID, buildIDPref);
});

View File

@ -1,12 +1,12 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Check that TelemetryPing notifies correctly on idle-daily.
// Check that TelemetrySession notifies correctly on idle-daily.
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
function run_test() {
do_test_pending();
@ -16,5 +16,5 @@ function run_test() {
do_test_finished();
}, "gather-telemetry", false);
TelemetryPing.observe(null, "idle-daily", null);
TelemetrySession.observe(null, "idle-daily", null);
}

View File

@ -22,6 +22,7 @@ Cu.import("resource://testing-common/httpd.js", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/TelemetryFile.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
Cu.import("resource://gre/modules/Task.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let {OS: {File, Path, Constants}} = Cu.import("resource://gre/modules/osfile.jsm", {});
@ -52,7 +53,7 @@ let gCreatedPings = 0;
let gSeenPings = 0;
/**
* Creates some TelemetryPings for the current session and
* Creates some TelemetrySession pings for the current session and
* saves them to disk. Each ping gets a unique ID slug based on
* an incrementor.
*
@ -65,14 +66,11 @@ let gSeenPings = 0;
*/
function createSavedPings(aNum, aAge) {
return Task.spawn(function*(){
// Create a TelemetryPing service that we can generate payloads from.
// Luckily, the TelemetryPing constructor does nothing that we need to
// clean up.
let pings = [];
let age = Date.now() - aAge;
for (let i = 0; i < aNum; ++i) {
let payload = TelemetryPing.getPayload();
let payload = TelemetrySession.getPayload();
let ping = { slug: "test-ping-" + gCreatedPings, reason: "test", payload: payload };
yield TelemetryFile.savePing(ping);
@ -117,7 +115,7 @@ function getSavePathForPing(aPing) {
}
/**
* Check if the number of TelemetryPings received by the
* Check if the number of TelemetrySession pings received by the
* HttpServer is not equal to aExpectedNum.
*
* @param aExpectedNum the number of pings we expect to receive.
@ -150,7 +148,7 @@ function assertNotSaved(aPings) {
/**
* Our handler function for the HttpServer that simply
* increments the gSeenPings global when it successfully
* receives and decodes a TelemetryPing payload.
* receives and decodes a TelemetrySession payload.
*
* @param aRequest the HTTP request sent from HttpServer.
*/
@ -173,11 +171,10 @@ function stopHttpServer() {
}
/**
* Teardown a TelemetryPing instance and clear out any pending
* pings to put as back in the starting state.
* Reset Telemetry state.
*/
function resetTelemetry() {
TelemetryPing.uninstall();
TelemetrySession.uninstall();
// Quick and dirty way to clear TelemetryFile's pendingPings
// collection, and put it back in its initial state.
let gen = TelemetryFile.popPendingPings();
@ -192,6 +189,10 @@ function startTelemetry() {
return TelemetryPing.setup();
}
function startTelemetrySession() {
return TelemetrySession.setup();
}
function run_test() {
gHttpServer.registerPrefixHandler("/submit/telemetry/", pingHandler);
gHttpServer.start(-1);
@ -213,6 +214,7 @@ function run_test() {
* immediately and never sent.
*/
add_task(function* test_expired_pings_are_deleted() {
yield startTelemetrySession();
let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
yield startTelemetry();
assertReceivedPings(0);
@ -224,6 +226,7 @@ add_task(function* test_expired_pings_are_deleted() {
* Test that really recent pings are not sent on Telemetry initialization.
*/
add_task(function* test_recent_pings_not_sent() {
yield startTelemetrySession();
let recentPings = yield createSavedPings(RECENT_PINGS);
yield startTelemetry();
assertReceivedPings(0);
@ -235,6 +238,7 @@ add_task(function* test_recent_pings_not_sent() {
* Test that only the most recent LRU_PINGS pings are kept at startup.
*/
add_task(function* test_most_recent_pings_kept() {
yield startTelemetrySession();
let head = yield createSavedPings(LRU_PINGS);
let tail = yield createSavedPings(3, ONE_MINUTE_MS);
let pings = head.concat(tail);
@ -259,6 +263,7 @@ add_task(function* test_most_recent_pings_kept() {
* should just be deleted.
*/
add_task(function* test_overdue_pings_trigger_send() {
yield startTelemetrySession();
let recentPings = yield createSavedPings(RECENT_PINGS);
let expiredPings = yield createSavedPings(EXPIRED_PINGS, EXPIRED_PING_FILE_AGE);
let overduePings = yield createSavedPings(OVERDUE_PINGS, OVERDUE_PING_FILE_AGE);

View File

@ -11,6 +11,7 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/TelemetryTimestamps.jsm");
Cu.import("resource://gre/modules/TelemetryPing.jsm");
Cu.import("resource://gre/modules/TelemetrySession.jsm");
const Telemetry = Services.telemetry;
const bundle = Services.strings.createBundle(
@ -950,7 +951,7 @@ function setupListeners() {
document.getElementById("late-writes-fetch-symbols").addEventListener("click",
function () {
let lateWrites = TelemetryPing.getPayload().lateWrites;
let lateWrites = TelemetrySession.getPayload().lateWrites;
let req = new SymbolicationRequest("late-writes",
LateWritesSingleton.renderHeader,
lateWrites.memoryMap,
@ -960,7 +961,7 @@ function setupListeners() {
document.getElementById("late-writes-hide-symbols").addEventListener("click",
function () {
let ping = TelemetryPing.getPayload();
let ping = TelemetrySession.getPayload();
LateWritesSingleton.renderLateWrites(ping.lateWrites);
}, false);
@ -1112,7 +1113,7 @@ function sortStartupMilestones(aSimpleMeasurements) {
}
function displayPingData() {
let ping = TelemetryPing.getPayload();
let ping = TelemetrySession.getPayload();
let keysHeader = bundle.GetStringFromName("keysHeader");
let valuesHeader = bundle.GetStringFromName("valuesHeader");

View File

@ -5,33 +5,30 @@ const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
// The @mozilla/xre/app-info;1 XPCOM object provided by the xpcshell test harness doesn't
// implement the nsIAppInfo interface, which is needed by Services.jsm and TelemetryPing.jsm.
// updateAppInfo() creates and registers a minimal mock app-info.
// implement the nsIAppInfo interface, which is needed by Services.jsm and
// TelemetrySession.jsm. updateAppInfo() creates and registers a minimal mock app-info.
Cu.import("resource://testing-common/AppInfo.jsm");
updateAppInfo();
function getSimpleMeasurementsFromTelemetryPing() {
return Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).
TelemetryPing.getPayload().simpleMeasurements;
return TelemetrySession.getPayload().simpleMeasurements;
}
function run_test() {
// Make profile available for |TelemetrySession.shutdown()|.
do_get_profile();
do_test_pending();
const Telemetry = Services.telemetry;
Telemetry.asyncFetchTelemetryData(function () {
try {
actualTest();
}
catch(e) {
do_throw("Failed: " + e);
}
do_test_finished();
});
Telemetry.asyncFetchTelemetryData(run_next_test);
}
function actualTest() {
add_task(function* actualTest() {
yield TelemetrySession.setup();
// Test the module logic
let tmp = {};
Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
@ -66,5 +63,9 @@ function actualTest() {
do_check_true(simpleMeasurements != null); // got simple measurements from ping data
do_check_true(simpleMeasurements.foo > 1); // foo was included
do_check_true(simpleMeasurements.bar > 1); // bar was included
do_check_null(simpleMeasurements.baz); // baz wasn't included since it wasn't added
}
do_check_eq(undefined, simpleMeasurements.baz); // baz wasn't included since it wasn't added
yield TelemetrySession.shutdown();
do_test_finished();
});

View File

@ -2396,7 +2396,9 @@ this.XPIProvider = {
}
catch (e) { }
Cu.import("resource://gre/modules/TelemetryPing.jsm", {}).TelemetryPing.setAddOns(data);
let TelemetrySession =
Cu.import("resource://gre/modules/TelemetrySession.jsm", {}).TelemetrySession;
TelemetrySession.setAddOns(data);
},
/**

View File

@ -13,8 +13,8 @@ const PREF_MIN_PLATFORM_COMPAT = "extensions.minCompatiblePlatformVersion
Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
// avoid the 'leaked window property' check
let scope = {};
Components.utils.import("resource://gre/modules/TelemetryPing.jsm", scope);
let TelemetryPing = scope.TelemetryPing;
Components.utils.import("resource://gre/modules/TelemetrySession.jsm", scope);
let TelemetrySession = scope.TelemetrySession;
/**
* Test add-ons:
@ -176,7 +176,7 @@ function get_list_names(aList) {
}
function check_telemetry({disabled, metaenabled, metadisabled, upgraded, failed, declined}) {
let ping = TelemetryPing.getPayload();
let ping = TelemetrySession.getPayload();
// info(JSON.stringify(ping));
let am = ping.simpleMeasurements.addonManager;
if (disabled !== undefined)
@ -193,6 +193,10 @@ function check_telemetry({disabled, metaenabled, metadisabled, upgraded, failed,
is(am.appUpdate_upgradeDeclined, declined, declined + " upgrades declined");
}
add_test(function test_setup() {
TelemetrySession.setup().then(run_next_test);
});
// Tests that the right add-ons show up in the mismatch dialog and updates can
// be installed
add_test(function basic_mismatch() {
@ -508,3 +512,7 @@ add_test(function overrides_retrieved() {
});
});
});
add_test(function test_shutdown() {
TelemetrySession.shutdown().then(run_next_test);
});