mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 06:35:42 +00:00
539 lines
16 KiB
JavaScript
539 lines
16 KiB
JavaScript
/* 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/.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ['ResourceStatsDB'];
|
|
|
|
const DEBUG = false;
|
|
function debug(s) { dump("-*- ResourceStatsDB: " + s + "\n"); }
|
|
|
|
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
|
|
|
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.importGlobalProperties(["indexedDB"]);
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "appsService",
|
|
"@mozilla.org/AppsService;1",
|
|
"nsIAppsService");
|
|
|
|
const DB_NAME = "resource_stats";
|
|
const DB_VERSION = 1;
|
|
const POWER_STATS_STORE = "power_stats_store";
|
|
const NETWORK_STATS_STORE = "network_stats_store";
|
|
const ALARM_STORE = "alarm_store";
|
|
|
|
const statsStoreNames = {
|
|
power: POWER_STATS_STORE,
|
|
network: NETWORK_STATS_STORE
|
|
};
|
|
|
|
// Constant defining the sampling rate.
|
|
const SAMPLE_RATE = 24 * 60 * 60 * 1000; // 1 day.
|
|
|
|
// Constant defining the MAX age of stored stats.
|
|
const MAX_STORAGE_AGE = 180 * SAMPLE_RATE; // 180 days.
|
|
|
|
this.ResourceStatsDB = function ResourceStatsDB() {
|
|
if (DEBUG) {
|
|
debug("Constructor()");
|
|
}
|
|
|
|
this.initDBHelper(DB_NAME, DB_VERSION,
|
|
[POWER_STATS_STORE, NETWORK_STATS_STORE, ALARM_STORE]);
|
|
};
|
|
|
|
ResourceStatsDB.prototype = {
|
|
__proto__: IndexedDBHelper.prototype,
|
|
|
|
_dbNewTxn: function(aStoreName, aTxnType, aCallback, aTxnCb) {
|
|
function successCb(aResult) {
|
|
aTxnCb(null, aResult);
|
|
}
|
|
function errorCb(aError) {
|
|
aTxnCb(aError, null);
|
|
}
|
|
return this.newTxn(aTxnType, aStoreName, aCallback, successCb, errorCb);
|
|
},
|
|
|
|
upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
|
|
if (DEBUG) {
|
|
debug("Upgrade DB from ver." + aOldVersion + " to ver." + aNewVersion);
|
|
}
|
|
|
|
let objectStore;
|
|
|
|
// Create PowerStatsStore.
|
|
objectStore = aDb.createObjectStore(POWER_STATS_STORE, {
|
|
keyPath: ["appId", "serviceType", "component", "timestamp"]
|
|
});
|
|
objectStore.createIndex("component", "component", { unique: false });
|
|
|
|
// Create NetworkStatsStore.
|
|
objectStore = aDb.createObjectStore(NETWORK_STATS_STORE, {
|
|
keyPath: ["appId", "serviceType", "component", "timestamp"]
|
|
});
|
|
objectStore.createIndex("component", "component", { unique: false });
|
|
|
|
// Create AlarmStore.
|
|
objectStore = aDb.createObjectStore(ALARM_STORE, {
|
|
keyPath: "alarmId",
|
|
autoIncrement: true
|
|
});
|
|
objectStore.createIndex("type", "type", { unique: false });
|
|
// Index for resource control target.
|
|
objectStore.createIndex("controlTarget",
|
|
["type", "manifestURL", "serviceType", "component"],
|
|
{ unique: false });
|
|
},
|
|
|
|
// Convert to UTC according to the current timezone and the filter timestamp
|
|
// to get SAMPLE_RATE precission.
|
|
_normalizeTime: function(aTime, aOffset) {
|
|
let time = Math.floor((aTime - aOffset) / SAMPLE_RATE) * SAMPLE_RATE;
|
|
|
|
return time;
|
|
},
|
|
|
|
/**
|
|
* aRecordArray contains an array of json objects storing network stats.
|
|
* The structure of the json object =
|
|
* {
|
|
* appId: XX,
|
|
* serviceType: "XX",
|
|
* componentStats: {
|
|
* "component_1": { receivedBytes: XX, sentBytes: XX },
|
|
* "component_2": { receivedBytes: XX, sentBytes: XX },
|
|
* ...
|
|
* },
|
|
* }
|
|
*/
|
|
saveNetworkStats: function(aRecordArray, aTimestamp, aResultCb) {
|
|
if (DEBUG) {
|
|
debug("saveNetworkStats()");
|
|
}
|
|
|
|
let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
|
|
let timestamp = this._normalizeTime(aTimestamp, offset);
|
|
|
|
this._dbNewTxn(NETWORK_STATS_STORE, "readwrite", function(aTxn, aStore) {
|
|
aRecordArray.forEach(function(aRecord) {
|
|
let stats = {
|
|
appId: aRecord.appId,
|
|
serviceType: aRecord.serviceType,
|
|
component: "",
|
|
timestamp: timestamp,
|
|
receivedBytes: 0,
|
|
sentBytes: 0
|
|
};
|
|
|
|
let totalReceivedBytes = 0;
|
|
let totalSentBytes = 0;
|
|
|
|
// Save stats of each component.
|
|
let data = aRecord.componentStats;
|
|
for (let component in data) {
|
|
// Save stats to database.
|
|
stats.component = component;
|
|
stats.receivedBytes = data[component].receivedBytes;
|
|
stats.sentBytes = data[component].sentBytes;
|
|
aStore.put(stats);
|
|
if (DEBUG) {
|
|
debug("Save network stats: " + JSON.stringify(stats));
|
|
}
|
|
|
|
// Accumulated to tatal stats.
|
|
totalReceivedBytes += stats.receivedBytes;
|
|
totalSentBytes += stats.sentBytes;
|
|
}
|
|
|
|
// Save total stats.
|
|
stats.component = "";
|
|
stats.receivedBytes = totalReceivedBytes;
|
|
stats.sentBytes = totalSentBytes;
|
|
aStore.put(stats);
|
|
if (DEBUG) {
|
|
debug("Save network stats: " + JSON.stringify(stats));
|
|
}
|
|
});
|
|
}, aResultCb);
|
|
},
|
|
|
|
/**
|
|
* aRecordArray contains an array of json objects storing power stats.
|
|
* The structure of the json object =
|
|
* {
|
|
* appId: XX,
|
|
* serviceType: "XX",
|
|
* componentStats: {
|
|
* "component_1": XX, // consumedPower
|
|
* "component_2": XX,
|
|
* ...
|
|
* },
|
|
* }
|
|
*/
|
|
savePowerStats: function(aRecordArray, aTimestamp, aResultCb) {
|
|
if (DEBUG) {
|
|
debug("savePowerStats()");
|
|
}
|
|
let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
|
|
let timestamp = this._normalizeTime(aTimestamp, offset);
|
|
|
|
this._dbNewTxn(POWER_STATS_STORE, "readwrite", function(aTxn, aStore) {
|
|
aRecordArray.forEach(function(aRecord) {
|
|
let stats = {
|
|
appId: aRecord.appId,
|
|
serviceType: aRecord.serviceType,
|
|
component: "",
|
|
timestamp: timestamp,
|
|
consumedPower: aRecord.totalStats
|
|
};
|
|
|
|
let totalConsumedPower = 0;
|
|
|
|
// Save stats of each component to database.
|
|
let data = aRecord.componentStats;
|
|
for (let component in data) {
|
|
// Save stats to database.
|
|
stats.component = component;
|
|
stats.consumedPower = data[component];
|
|
aStore.put(stats);
|
|
if (DEBUG) {
|
|
debug("Save power stats: " + JSON.stringify(stats));
|
|
}
|
|
// Accumulated to total stats.
|
|
totalConsumedPower += stats.consumedPower;
|
|
}
|
|
|
|
// Save total stats.
|
|
stats.component = "";
|
|
stats.consumedPower = totalConsumedPower;
|
|
aStore.put(stats);
|
|
if (DEBUG) {
|
|
debug("Save power stats: " + JSON.stringify(stats));
|
|
}
|
|
});
|
|
}, aResultCb);
|
|
},
|
|
|
|
// Get stats from a store.
|
|
getStats: function(aType, aManifestURL, aServiceType, aComponent,
|
|
aStart, aEnd, aResultCb) {
|
|
if (DEBUG) {
|
|
debug(aType + "Mgr.getStats()");
|
|
}
|
|
|
|
let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
|
|
|
|
// Get appId and check whether manifestURL is a valid app.
|
|
let appId = 0;
|
|
if (aManifestURL) {
|
|
appId = appsService.getAppLocalIdByManifestURL(aManifestURL);
|
|
|
|
if (!appId) {
|
|
aResultCb("Invalid manifestURL", null);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get store name.
|
|
let storeName = statsStoreNames[aType];
|
|
|
|
// Normalize start time and end time to SAMPLE_RATE precission.
|
|
let start = this._normalizeTime(aStart, offset);
|
|
let end = this._normalizeTime(aEnd, offset);
|
|
if (DEBUG) {
|
|
debug("Query time range: " + start + " to " + end);
|
|
debug("[appId, serviceType, component] = [" + appId + ", " + aServiceType
|
|
+ ", " + aComponent + "]");
|
|
}
|
|
|
|
// Create filters.
|
|
let lowerFilter = [appId, aServiceType, aComponent, start];
|
|
let upperFilter = [appId, aServiceType, aComponent, end];
|
|
|
|
// Execute DB query.
|
|
this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
|
|
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
|
|
|
|
let statsData = [];
|
|
|
|
if (!aTxn.result) {
|
|
aTxn.result = Object.create(null);
|
|
}
|
|
aTxn.result.type = aType;
|
|
aTxn.result.component = aComponent;
|
|
aTxn.result.serviceType = aServiceType;
|
|
aTxn.result.manifestURL = aManifestURL;
|
|
aTxn.result.start = start + offset;
|
|
aTxn.result.end = end + offset;
|
|
// Since ResourceStats() would require SAMPLE_RATE when filling the empty
|
|
// entries of statsData array, we append SAMPLE_RATE to the result field
|
|
// to save an IPC call.
|
|
aTxn.result.sampleRate = SAMPLE_RATE;
|
|
|
|
let request = aStore.openCursor(range, "prev");
|
|
if (aType == "power") {
|
|
request.onsuccess = function(aEvent) {
|
|
var cursor = aEvent.target.result;
|
|
if (cursor) {
|
|
if (DEBUG) {
|
|
debug("Get " + JSON.stringify(cursor.value));
|
|
}
|
|
|
|
// Covert timestamp to current timezone.
|
|
statsData.push({
|
|
timestamp: cursor.value.timestamp + offset,
|
|
consumedPower: cursor.value.consumedPower
|
|
});
|
|
cursor.continue();
|
|
return;
|
|
}
|
|
aTxn.result.statsData = statsData;
|
|
};
|
|
} else if (aType == "network") {
|
|
request.onsuccess = function(aEvent) {
|
|
var cursor = aEvent.target.result;
|
|
if (cursor) {
|
|
if (DEBUG) {
|
|
debug("Get " + JSON.stringify(cursor.value));
|
|
}
|
|
|
|
// Covert timestamp to current timezone.
|
|
statsData.push({
|
|
timestamp: cursor.value.timestamp + offset,
|
|
receivedBytes: cursor.value.receivedBytes,
|
|
sentBytes: cursor.value.sentBytes
|
|
});
|
|
cursor.continue();
|
|
return;
|
|
}
|
|
aTxn.result.statsData = statsData;
|
|
};
|
|
}
|
|
}, aResultCb);
|
|
},
|
|
|
|
// Delete the stats of a specific app/service (within a specified time range).
|
|
clearStats: function(aType, aAppId, aServiceType, aComponent,
|
|
aStart, aEnd, aResultCb) {
|
|
if (DEBUG) {
|
|
debug(aType + "Mgr.clearStats()");
|
|
}
|
|
|
|
let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
|
|
|
|
// Get store name.
|
|
let storeName = statsStoreNames[aType];
|
|
|
|
// Normalize start and end time to SAMPLE_RATE precission.
|
|
let start = this._normalizeTime(aStart, offset);
|
|
let end = this._normalizeTime(aEnd, offset);
|
|
if (DEBUG) {
|
|
debug("Query time range: " + start + " to " + end);
|
|
debug("[appId, serviceType, component] = [" + aAppId + ", " + aServiceType
|
|
+ ", " + aComponent + "]");
|
|
}
|
|
|
|
// Create filters.
|
|
let lowerFilter = [aAppId, aServiceType, aComponent, start];
|
|
let upperFilter = [aAppId, aServiceType, aComponent, end];
|
|
|
|
// Execute clear operation.
|
|
this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
|
|
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
|
|
let request = aStore.openCursor(range).onsuccess = function(aEvent) {
|
|
let cursor = aEvent.target.result;
|
|
if (cursor) {
|
|
if (DEBUG) {
|
|
debug("Delete " + JSON.stringify(cursor.value) + " from database");
|
|
}
|
|
cursor.delete();
|
|
cursor.continue();
|
|
return;
|
|
}
|
|
};
|
|
}, aResultCb);
|
|
},
|
|
|
|
// Delete all stats saved in a store.
|
|
clearAllStats: function(aType, aResultCb) {
|
|
if (DEBUG) {
|
|
debug(aType + "Mgr.clearAllStats()");
|
|
}
|
|
|
|
let storeName = statsStoreNames[aType];
|
|
|
|
// Execute clear operation.
|
|
this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
|
|
if (DEBUG) {
|
|
debug("Clear " + aType + " stats from datastore");
|
|
}
|
|
aStore.clear();
|
|
}, aResultCb);
|
|
},
|
|
|
|
addAlarm: function(aAlarm, aResultCb) {
|
|
if (DEBUG) {
|
|
debug(aAlarm.type + "Mgr.addAlarm()");
|
|
debug("alarm = " + JSON.stringify(aAlarm));
|
|
}
|
|
|
|
this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
|
|
aStore.put(aAlarm).onsuccess = function setResult(aEvent) {
|
|
// Get alarmId.
|
|
aTxn.result = aEvent.target.result;
|
|
if (DEBUG) {
|
|
debug("New alarm ID: " + aTxn.result);
|
|
}
|
|
};
|
|
}, aResultCb);
|
|
},
|
|
|
|
// Convert DB record to alarm object.
|
|
_recordToAlarm: function(aRecord) {
|
|
let alarm = {
|
|
alarmId: aRecord.alarmId,
|
|
type: aRecord.type,
|
|
component: aRecord.component,
|
|
serviceType: aRecord.serviceType,
|
|
manifestURL: aRecord.manifestURL,
|
|
threshold: aRecord.threshold,
|
|
data: aRecord.data
|
|
};
|
|
|
|
return alarm;
|
|
},
|
|
|
|
getAlarms: function(aType, aOptions, aResultCb) {
|
|
if (DEBUG) {
|
|
debug(aType + "Mgr.getAlarms()");
|
|
debug("[appId, serviceType, component] = [" + aOptions.appId + ", "
|
|
+ aOptions.serviceType + ", " + aOptions.component + "]");
|
|
}
|
|
|
|
// Execute clear operation.
|
|
this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
|
|
if (!aTxn.result) {
|
|
aTxn.result = [];
|
|
}
|
|
|
|
let indexName = null;
|
|
let range = null;
|
|
|
|
if (aOptions) { // Get alarms associated to specified statsOptions.
|
|
indexName = "controlTarget";
|
|
range = IDBKeyRange.only([aType, aOptions.manifestURL,
|
|
aOptions.serviceType, aOptions.component]);
|
|
} else { // Get all alarms of the specified type.
|
|
indexName = "type";
|
|
range = IDBKeyRange.only(aType);
|
|
}
|
|
|
|
let request = aStore.index(indexName).openCursor(range);
|
|
request.onsuccess = function onsuccess(aEvent) {
|
|
let cursor = aEvent.target.result;
|
|
if (cursor) {
|
|
aTxn.result.push(this._recordToAlarm(cursor.value));
|
|
cursor.continue();
|
|
return;
|
|
}
|
|
}.bind(this);
|
|
}.bind(this), aResultCb);
|
|
},
|
|
|
|
removeAlarm: function(aType, aAlarmId, aResultCb) {
|
|
if (DEBUG) {
|
|
debug("removeAlarms(" + aAlarmId + ")");
|
|
}
|
|
|
|
// Execute clear operation.
|
|
this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
|
|
aStore.get(aAlarmId).onsuccess = function onsuccess(aEvent) {
|
|
let alarm = aEvent.target.result;
|
|
aTxn.result = false;
|
|
if (!alarm || aType !== alarm.type) {
|
|
return;
|
|
}
|
|
|
|
if (DEBUG) {
|
|
debug("Remove alarm " + JSON.stringify(alarm) + " from datastore");
|
|
}
|
|
aStore.delete(aAlarmId);
|
|
aTxn.result = true;
|
|
};
|
|
}, aResultCb);
|
|
},
|
|
|
|
removeAllAlarms: function(aType, aResultCb) {
|
|
if (DEBUG) {
|
|
debug(aType + "Mgr.removeAllAlarms()");
|
|
}
|
|
|
|
// Execute clear operation.
|
|
this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
|
|
if (DEBUG) {
|
|
debug("Remove all " + aType + " alarms from datastore.");
|
|
}
|
|
|
|
let range = IDBKeyRange.only(aType);
|
|
let request = aStore.index("type").openCursor(range);
|
|
request.onsuccess = function onsuccess(aEvent) {
|
|
let cursor = aEvent.target.result;
|
|
if (cursor) {
|
|
if (DEBUG) {
|
|
debug("Remove " + JSON.stringify(cursor.value) + " from database.");
|
|
}
|
|
cursor.delete();
|
|
cursor.continue();
|
|
return;
|
|
}
|
|
};
|
|
}, aResultCb);
|
|
},
|
|
|
|
// Get all index keys of the component.
|
|
getComponents: function(aType, aResultCb) {
|
|
if (DEBUG) {
|
|
debug(aType + "Mgr.getComponents()");
|
|
}
|
|
|
|
let storeName = statsStoreNames[aType];
|
|
|
|
this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
|
|
if (!aTxn.result) {
|
|
aTxn.result = [];
|
|
}
|
|
|
|
let request = aStore.index("component").openKeyCursor(null, "nextunique");
|
|
request.onsuccess = function onsuccess(aEvent) {
|
|
let cursor = aEvent.target.result;
|
|
if (cursor) {
|
|
aTxn.result.push(cursor.key);
|
|
cursor.continue();
|
|
return;
|
|
}
|
|
|
|
// Remove "" from the result, which indicates sum of all
|
|
// components' stats.
|
|
let index = aTxn.result.indexOf("");
|
|
if (index > -1) {
|
|
aTxn.result.splice(index, 1);
|
|
}
|
|
};
|
|
}, aResultCb);
|
|
},
|
|
|
|
get sampleRate () {
|
|
return SAMPLE_RATE;
|
|
},
|
|
|
|
get maxStorageAge() {
|
|
return MAX_STORAGE_AGE;
|
|
},
|
|
};
|
|
|