diff --git a/toolkit/components/telemetry/Telemetry.cpp b/toolkit/components/telemetry/Telemetry.cpp index 882aeb5b105f..921a01ccae7c 100644 --- a/toolkit/components/telemetry/Telemetry.cpp +++ b/toolkit/components/telemetry/Telemetry.cpp @@ -1189,6 +1189,346 @@ TelemetryImpl::GetHistogramById(const nsACString &name, JSContext *cx, jsval *re return WrapAndReturnHistogram(h, cx, ret); } +class TelemetrySessionData : public nsITelemetrySessionData +{ + NS_DECL_ISUPPORTS + NS_DECL_NSITELEMETRYSESSIONDATA + +public: + static nsresult LoadFromDisk(nsIFile *, TelemetrySessionData **ptr); + static nsresult SaveToDisk(nsIFile *, const nsACString &uuid); + + TelemetrySessionData(const char *uuid); + ~TelemetrySessionData(); + +private: + typedef nsBaseHashtableET EntryType; + typedef AutoHashtable SessionMapType; + static bool SampleReflector(EntryType *entry, JSContext *cx, JSObject *obj); + SessionMapType mSampleSetMap; + nsCString mUUID; + + bool DeserializeHistogramData(Pickle &pickle, void **iter); + static bool SerializeHistogramData(Pickle &pickle); + + // The file format version. Should be incremented whenever we change + // how individual SampleSets are stored in the file. + static const unsigned int sVersion = 1; +}; + +NS_IMPL_THREADSAFE_ISUPPORTS1(TelemetrySessionData, nsITelemetrySessionData) + +TelemetrySessionData::TelemetrySessionData(const char *uuid) + : mUUID(uuid) +{ +} + +TelemetrySessionData::~TelemetrySessionData() +{ +} + +NS_IMETHODIMP +TelemetrySessionData::GetUuid(nsACString &uuid) +{ + uuid = mUUID; + return NS_OK; +} + +bool +TelemetrySessionData::SampleReflector(EntryType *entry, JSContext *cx, + JSObject *snapshots) +{ + // Don't reflect histograms with no data associated with them. + if (entry->mData.sum() == 0) { + return true; + } + + // This has the undesirable effect of creating a histogram for the + // current session with the given ID. But there's no good way to + // compute the ranges and buckets from scratch. + Histogram *h = nsnull; + nsresult rv = GetHistogramByEnumId(Telemetry::ID(entry->GetKey()), &h); + if (NS_FAILED(rv)) { + return true; + } + + JSObject *snapshot = JS_NewObject(cx, NULL, NULL, NULL); + if (!snapshot) { + return false; + } + JS::AutoObjectRooter root(cx, snapshot); + switch (ReflectHistogramAndSamples(cx, snapshot, h, entry->mData)) { + case REFLECT_OK: + return JS_DefineProperty(cx, snapshots, + h->histogram_name().c_str(), + OBJECT_TO_JSVAL(snapshot), NULL, NULL, + JSPROP_ENUMERATE); + case REFLECT_CORRUPT: + // Just ignore this one. + return true; + case REFLECT_FAILURE: + return false; + default: + MOZ_NOT_REACHED("unhandled reflection status"); + return false; + } +} + +NS_IMETHODIMP +TelemetrySessionData::GetSnapshots(JSContext *cx, jsval *ret) +{ + JSObject *snapshots = JS_NewObject(cx, NULL, NULL, NULL); + if (!snapshots) { + return NS_ERROR_FAILURE; + } + JS::AutoObjectRooter root(cx, snapshots); + + if (!mSampleSetMap.ReflectHashtable(SampleReflector, cx, snapshots)) { + return NS_ERROR_FAILURE; + } + + *ret = OBJECT_TO_JSVAL(snapshots); + return NS_OK; +} + +bool +TelemetrySessionData::DeserializeHistogramData(Pickle &pickle, void **iter) +{ + uint32_t count = 0; + if (!pickle.ReadUInt32(iter, &count)) { + return false; + } + + for (size_t i = 0; i < count; ++i) { + int stored_length; + const char *name; + if (!pickle.ReadData(iter, &name, &stored_length)) { + return false; + } + + Telemetry::ID id; + nsresult rv = TelemetryImpl::GetHistogramEnumId(name, &id); + if (NS_FAILED(rv)) { + // We serialized a non-static histogram or we serialized a + // histogram that is no longer defined in TelemetryHistograms.h. + // Just drop its data on the floor. If we can't deserialize the + // data, though, we're in trouble. + Histogram::SampleSet ss; + if (!ss.Deserialize(iter, pickle)) { + return false; + } + } else { + EntryType *entry = mSampleSetMap.GetEntry(id); + if (!entry) { + entry = mSampleSetMap.PutEntry(id); + if (NS_UNLIKELY(!entry)) { + return false; + } + if (!entry->mData.Deserialize(iter, pickle)) { + return false; + } + } + } + } + + return true; +} + +nsresult +TelemetrySessionData::LoadFromDisk(nsIFile *file, TelemetrySessionData **ptr) +{ + *ptr = nsnull; + nsresult rv; + + AutoFDClose fd; + rv = file->OpenNSPRFileDesc(PR_RDONLY, 0, &fd.rwget()); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + // If there's not even enough data to read the header for the pickle, + // don't bother. Conveniently, this handles the error case as well. + int32_t size = PR_Available(fd); + if (size < static_cast(sizeof(Pickle::Header))) { + return NS_ERROR_FAILURE; + } + + nsAutoArrayPtr data(new char[size]); + int32_t amount = PR_Read(fd, data, size); + if (amount != size) { + return NS_ERROR_FAILURE; + } + + Pickle pickle(data, size); + void *iter = NULL; + + // Make sure that how much data the pickle thinks it has corresponds + // with how much data we actually read. + const Pickle::Header *header = pickle.headerT(); + if (header->payload_size != static_cast(amount) - sizeof(*header)) { + return NS_ERROR_FAILURE; + } + + unsigned int storedVersion; + if (!(pickle.ReadUInt32(&iter, &storedVersion) + && storedVersion == sVersion)) { + return NS_ERROR_FAILURE; + } + + const char *uuid; + int uuidLength; + if (!pickle.ReadData(&iter, &uuid, &uuidLength)) { + return NS_ERROR_FAILURE; + } + + nsAutoPtr sessionData(new TelemetrySessionData(uuid)); + if (!sessionData->DeserializeHistogramData(pickle, &iter)) { + return NS_ERROR_FAILURE; + } + + *ptr = sessionData.forget(); + return NS_OK; +} + +bool +TelemetrySessionData::SerializeHistogramData(Pickle &pickle) +{ + StatisticsRecorder::Histograms hs; + StatisticsRecorder::GetHistograms(&hs); + + if (!pickle.WriteUInt32(hs.size())) { + return false; + } + + for (StatisticsRecorder::Histograms::const_iterator it = hs.begin(); + it != hs.end(); + ++it) { + const Histogram *h = *it; + const char *name = h->histogram_name().c_str(); + + // We don't check IsEmpty(h) here. We discard no-data histograms on + // read-in, instead. It's easier to write out the number of + // histograms required that way. (The pickle interface doesn't make + // it easy to go back and overwrite previous data.) + + Histogram::SampleSet ss; + h->SnapshotSample(&ss); + + if (!(pickle.WriteData(name, strlen(name)+1) + && ss.Serialize(&pickle))) { + return false; + } + } + + return true; +} + +nsresult +TelemetrySessionData::SaveToDisk(nsIFile *file, const nsACString &uuid) +{ + nsresult rv; + + AutoFDClose fd; + rv = file->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0600, &fd.rwget()); + if (NS_FAILED(rv)) { + return rv; + } + + Pickle pickle; + if (!pickle.WriteUInt32(sVersion)) { + return NS_ERROR_FAILURE; + } + + // Include the trailing NULL for the UUID to make reading easier. + const char *data; + size_t length = uuid.GetData(&data); + if (!(pickle.WriteData(data, length+1) + && SerializeHistogramData(pickle))) { + return NS_ERROR_FAILURE; + } + + int32_t amount = PR_Write(fd, static_cast(pickle.data()), + pickle.size()); + if (amount != pickle.size()) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +class SaveHistogramEvent : public nsRunnable +{ +public: + SaveHistogramEvent(nsIFile *file, const nsACString &uuid, + nsITelemetrySaveSessionDataCallback *callback) + : mFile(file), mUUID(uuid), mCallback(callback) + {} + + NS_IMETHOD Run() + { + nsresult rv = TelemetrySessionData::SaveToDisk(mFile, mUUID); + mCallback->Handle(!!NS_SUCCEEDED(rv)); + return rv; + } + +private: + nsCOMPtr mFile; + nsCString mUUID; + nsCOMPtr mCallback; +}; + +NS_IMETHODIMP +TelemetryImpl::SaveHistograms(nsIFile *file, const nsACString &uuid, + nsITelemetrySaveSessionDataCallback *callback, + bool isSynchronous) +{ + nsCOMPtr event = new SaveHistogramEvent(file, uuid, callback); + if (isSynchronous) { + return event ? event->Run() : NS_ERROR_FAILURE; + } else { + return NS_DispatchToCurrentThread(event); + } +} + +class LoadHistogramEvent : public nsRunnable +{ +public: + LoadHistogramEvent(nsIFile *file, + nsITelemetryLoadSessionDataCallback *callback) + : mFile(file), mCallback(callback) + {} + + NS_IMETHOD Run() + { + TelemetrySessionData *sessionData = nsnull; + nsresult rv = TelemetrySessionData::LoadFromDisk(mFile, &sessionData); + if (NS_FAILED(rv)) { + mCallback->Handle(nsnull); + } else { + nsCOMPtr data(sessionData); + mCallback->Handle(data); + } + return rv; + } + +private: + nsCOMPtr mFile; + nsCOMPtr mCallback; +}; + +NS_IMETHODIMP +TelemetryImpl::LoadHistograms(nsIFile *file, + nsITelemetryLoadSessionDataCallback *callback, + bool isSynchronous) +{ + nsCOMPtr event = new LoadHistogramEvent(file, callback); + if (isSynchronous) { + return event ? event->Run() : NS_ERROR_FAILURE; + } else { + return NS_DispatchToCurrentThread(event); + } +} + NS_IMETHODIMP TelemetryImpl::GetCanRecord(bool *ret) { *ret = mCanRecord; diff --git a/toolkit/components/telemetry/nsITelemetry.idl b/toolkit/components/telemetry/nsITelemetry.idl index ccbead035af3..671029a9502f 100644 --- a/toolkit/components/telemetry/nsITelemetry.idl +++ b/toolkit/components/telemetry/nsITelemetry.idl @@ -6,7 +6,37 @@ #include "nsISupports.idl" #include "nsIFile.idl" -[scriptable, uuid(de54f594-4c20-4968-a27a-83b38ff952b9)] +[scriptable, uuid(02719ffb-1a87-46cd-b8d3-5583f3267b32)] +interface nsITelemetrySessionData : nsISupports +{ + /** + * The UUID of our previous session. + */ + readonly attribute ACString uuid; + + /** + * An object containing a snapshot from all registered histograms that had + * data recorded in the previous session. + * { name1: data1, name2: data2, .... } + * where the individual dataN are as nsITelemetry.histogramSnapshots. + */ + [implicit_jscontext] + readonly attribute jsval snapshots; +}; + +[scriptable, function, uuid(aff36c9d-7e4c-41ab-a9b6-53773bbca0cd)] +interface nsITelemetryLoadSessionDataCallback : nsISupports +{ + void handle(in nsITelemetrySessionData data); +}; + +[scriptable, function, uuid(40065f26-afd2-4417-93de-c1de9adb1548)] +interface nsITelemetrySaveSessionDataCallback : nsISupports +{ + void handle(in bool success); +}; + +[scriptable, uuid(f23a2c8d-9286-42e9-ab1b-ed287eeade6d)] interface nsITelemetry : nsISupports { /** @@ -118,6 +148,33 @@ interface nsITelemetry : nsISupports [implicit_jscontext] jsval getHistogramById(in ACString id); + /** + * Save persistent histograms to the given file. + * + * @param file - filename for saving + * @param uuid - UUID of this session + * @param callback - function to be caled when file writing is complete + */ + void saveHistograms(in nsIFile file, in ACString uuid, + in nsITelemetrySaveSessionDataCallback callback, + in bool isSynchronous); + + /* Reconstruct an nsITelemetryDataSession object containing histogram + * information from the given file; the file must have been produced + * via saveHistograms. + * + * This method does not modify the histogram information being + * collected in the current session. + * + * The reconstructed object is then passed to the given callback. + * + * @param file - the file to load histogram information from + * @param callback - function to process histogram information + */ + void loadHistograms(in nsIFile file, + in nsITelemetryLoadSessionDataCallback callback, + in bool isSynchronous); + /** * Set this to false to disable gathering of telemetry statistics. */ diff --git a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js index 976084908434..b96a74d5dc1d 100644 --- a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js +++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js @@ -286,6 +286,39 @@ function generateUUID() { return str.substring(1, str.length - 1); } +// Check that we do sane things when saving to disk. +function test_loadSave() +{ + let dirService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + let tmpDir = dirService.get("TmpD", Ci.nsILocalFile); + let tmpFile = tmpDir.clone(); + tmpFile.append("saved-histograms.dat"); + if (tmpFile.exists()) { + tmpFile.remove(true); + } + + let saveFinished = false; + let loadFinished = false; + let uuid = generateUUID(); + let loadCallback = function(data) { + do_check_true(data != null); + do_check_eq(uuid, data.uuid); + loadFinished = true; + do_test_finished(); + }; + let saveCallback = function(success) { + do_check_true(success); + Telemetry.loadHistograms(tmpFile, loadCallback, false); + saveFinished = true; + }; + do_test_pending(); + Telemetry.saveHistograms(tmpFile, uuid, saveCallback, false); + do_register_cleanup(function () do_check_true(saveFinished)); + do_register_cleanup(function () do_check_true(loadFinished)); + do_register_cleanup(function () tmpFile.remove(true)); +} + function run_test() { let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR] @@ -309,4 +342,5 @@ function run_test() test_getSlowSQL(); test_privateMode(); test_addons(); + test_loadSave(); }