| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //------------------------------------------------------------------------------ |
| // Description of the life cycle of a instance of MetricsService. |
| // |
| // OVERVIEW |
| // |
| // A MetricsService instance is created at ChromeFrame startup in |
| // the IE process. It is the central controller for the UMA log data. |
| // Its major job is to manage logs, prepare them for transmission. |
| // Currently only histogram data is tracked in log. When MetricsService |
| // prepares log for submission it snapshots the current stats of histograms, |
| // translates log to a protocol buffer. Transmission includes submitting a |
| // compressed log as data in a URL-get, and is performed using functionality |
| // provided by Urlmon |
| // The actual transmission is performed using a windows timer procedure which |
| // basically means that the thread on which the MetricsService object is |
| // instantiated needs a message pump. Also on IE7 where every tab is created |
| // on its own thread we would have a case where the timer procedures can |
| // compete for sending histograms. |
| // |
| // When preparing log for submission we acquire a list of all local histograms |
| // that have been flagged for upload to the UMA server. |
| // |
| // When ChromeFrame shuts down, there will typically be a fragment of an ongoing |
| // log that has not yet been transmitted. Currently this data is ignored. |
| // |
| // With the above overview, we can now describe the state machine's various |
| // stats, based on the State enum specified in the state_ member. Those states |
| // are: |
| // |
| // INITIALIZED, // Constructor was called. |
| // ACTIVE, // Accumalating log data. |
| // STOPPED, // Service has stopped. |
| // |
| //----------------------------------------------------------------------------- |
| |
| #include "chrome_frame/metrics_service.h" |
| |
| #include <atlbase.h> |
| #include <atlwin.h> |
| #include <objbase.h> |
| #include <windows.h> |
| |
| #include "base/metrics/statistics_recorder.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/lock.h" |
| #include "base/win/scoped_comptr.h" |
| #include "chrome/common/chrome_version_info.h" |
| #include "chrome/common/metrics/metrics_log_base.h" |
| #include "chrome/common/metrics/metrics_log_manager.h" |
| #include "chrome/installer/util/browser_distribution.h" |
| #include "chrome/installer/util/google_update_settings.h" |
| #include "chrome_frame/bind_status_callback_impl.h" |
| #include "chrome_frame/crash_reporting/crash_metrics.h" |
| #include "chrome_frame/html_utils.h" |
| #include "chrome_frame/utils.h" |
| |
| using base::Time; |
| using base::TimeDelta; |
| using base::win::ScopedComPtr; |
| |
| // The first UMA upload occurs after this interval. |
| static const int kInitialUMAUploadTimeoutMilliSeconds = 30000; |
| |
| // Default to one UMA upload per 10 mins. |
| static const int kMinMilliSecondsPerUMAUpload = 600000; |
| |
| base::LazyInstance<base::ThreadLocalPointer<MetricsService> > |
| MetricsService::g_metrics_instance_ = LAZY_INSTANCE_INITIALIZER; |
| |
| std::string MetricsService::client_id_; |
| |
| base::Lock MetricsService::metrics_service_lock_; |
| |
| // This class provides functionality to upload the ChromeFrame UMA data to the |
| // server. An instance of this class is created whenever we have data to be |
| // uploaded to the server. |
| class ChromeFrameMetricsDataUploader : public BSCBImpl { |
| public: |
| ChromeFrameMetricsDataUploader() |
| : cache_stream_(NULL), |
| upload_data_size_(0) { |
| DVLOG(1) << __FUNCTION__; |
| } |
| |
| ~ChromeFrameMetricsDataUploader() { |
| DVLOG(1) << __FUNCTION__; |
| } |
| |
| static HRESULT UploadDataHelper( |
| const std::string& upload_data, |
| const std::string& server_url, |
| const std::string& mime_type) { |
| CComObject<ChromeFrameMetricsDataUploader>* data_uploader = NULL; |
| CComObject<ChromeFrameMetricsDataUploader>::CreateInstance(&data_uploader); |
| DCHECK(data_uploader != NULL); |
| |
| data_uploader->AddRef(); |
| HRESULT hr = data_uploader->UploadData(upload_data, server_url, mime_type); |
| if (FAILED(hr)) { |
| DLOG(ERROR) << "Failed to initialize ChromeFrame UMA data uploader: Err" |
| << hr; |
| } |
| data_uploader->Release(); |
| return hr; |
| } |
| |
| HRESULT UploadData(const std::string& upload_data, |
| const std::string& server_url, |
| const std::string& mime_type) { |
| if (upload_data.empty()) { |
| NOTREACHED() << "Invalid upload data"; |
| return E_INVALIDARG; |
| } |
| |
| DCHECK(cache_stream_.get() == NULL); |
| |
| upload_data_size_ = upload_data.size() + 1; |
| |
| HRESULT hr = CreateStreamOnHGlobal(NULL, TRUE, cache_stream_.Receive()); |
| if (FAILED(hr)) { |
| NOTREACHED() << "Failed to create stream. Error:" |
| << hr; |
| return hr; |
| } |
| |
| DCHECK(cache_stream_.get()); |
| |
| unsigned long written = 0; |
| cache_stream_->Write(upload_data.c_str(), upload_data_size_, &written); |
| DCHECK(written == upload_data_size_); |
| |
| RewindStream(cache_stream_); |
| |
| server_url_ = ASCIIToWide(server_url); |
| mime_type_ = mime_type; |
| DCHECK(!server_url_.empty()); |
| DCHECK(!mime_type_.empty()); |
| |
| hr = CreateURLMoniker(NULL, server_url_.c_str(), |
| upload_moniker_.Receive()); |
| if (FAILED(hr)) { |
| DLOG(ERROR) << "Failed to create url moniker for url:" |
| << server_url_.c_str() |
| << " Error:" |
| << hr; |
| } else { |
| ScopedComPtr<IBindCtx> context; |
| hr = CreateAsyncBindCtx(0, this, NULL, context.Receive()); |
| DCHECK(SUCCEEDED(hr)); |
| DCHECK(context); |
| |
| ScopedComPtr<IStream> stream; |
| hr = upload_moniker_->BindToStorage( |
| context, NULL, IID_IStream, |
| reinterpret_cast<void**>(stream.Receive())); |
| if (FAILED(hr)) { |
| NOTREACHED(); |
| DLOG(ERROR) << "Failed to bind to upload data moniker. Error:" |
| << hr; |
| } |
| } |
| return hr; |
| } |
| |
| STDMETHOD(BeginningTransaction)(LPCWSTR url, LPCWSTR headers, DWORD reserved, |
| LPWSTR* additional_headers) { |
| std::string new_headers; |
| new_headers = |
| base::StringPrintf( |
| "Content-Length: %s\r\n" |
| "Content-Type: %s\r\n" |
| "%s\r\n", |
| base::Int64ToString(upload_data_size_).c_str(), |
| mime_type_.c_str(), |
| http_utils::GetDefaultUserAgentHeaderWithCFTag().c_str()); |
| |
| *additional_headers = reinterpret_cast<wchar_t*>( |
| CoTaskMemAlloc((new_headers.size() + 1) * sizeof(wchar_t))); |
| |
| lstrcpynW(*additional_headers, ASCIIToWide(new_headers).c_str(), |
| new_headers.size()); |
| |
| return BSCBImpl::BeginningTransaction(url, headers, reserved, |
| additional_headers); |
| } |
| |
| STDMETHOD(GetBindInfo)(DWORD* bind_flags, BINDINFO* bind_info) { |
| if ((bind_info == NULL) || (bind_info->cbSize == 0) || |
| (bind_flags == NULL)) |
| return E_INVALIDARG; |
| |
| *bind_flags = BINDF_ASYNCHRONOUS | BINDF_ASYNCSTORAGE | BINDF_PULLDATA; |
| // Bypass caching proxies on POSTs and PUTs and avoid writing responses to |
| // these requests to the browser's cache |
| *bind_flags |= BINDF_GETNEWESTVERSION | BINDF_PRAGMA_NO_CACHE; |
| |
| DCHECK(cache_stream_.get()); |
| |
| // Initialize the STGMEDIUM. |
| memset(&bind_info->stgmedData, 0, sizeof(STGMEDIUM)); |
| bind_info->grfBindInfoF = 0; |
| bind_info->szCustomVerb = NULL; |
| bind_info->dwBindVerb = BINDVERB_POST; |
| bind_info->stgmedData.tymed = TYMED_ISTREAM; |
| bind_info->stgmedData.pstm = cache_stream_.get(); |
| bind_info->stgmedData.pstm->AddRef(); |
| return BSCBImpl::GetBindInfo(bind_flags, bind_info); |
| } |
| |
| STDMETHOD(OnResponse)(DWORD response_code, LPCWSTR response_headers, |
| LPCWSTR request_headers, LPWSTR* additional_headers) { |
| DVLOG(1) << __FUNCTION__ << " headers: \n" << response_headers; |
| return BSCBImpl::OnResponse(response_code, response_headers, |
| request_headers, additional_headers); |
| } |
| |
| private: |
| std::wstring server_url_; |
| std::string mime_type_; |
| size_t upload_data_size_; |
| ScopedComPtr<IStream> cache_stream_; |
| ScopedComPtr<IMoniker> upload_moniker_; |
| }; |
| |
| MetricsService* MetricsService::GetInstance() { |
| if (g_metrics_instance_.Pointer()->Get()) |
| return g_metrics_instance_.Pointer()->Get(); |
| |
| g_metrics_instance_.Pointer()->Set(new MetricsService); |
| return g_metrics_instance_.Pointer()->Get(); |
| } |
| |
| MetricsService::MetricsService() |
| : recording_active_(false), |
| reporting_active_(false), |
| user_permits_upload_(false), |
| state_(INITIALIZED), |
| thread_(NULL), |
| initial_uma_upload_(true), |
| transmission_timer_id_(0) { |
| } |
| |
| MetricsService::~MetricsService() { |
| SetRecording(false); |
| } |
| |
| void MetricsService::InitializeMetricsState() { |
| DCHECK(state_ == INITIALIZED); |
| |
| thread_ = base::PlatformThread::CurrentId(); |
| |
| user_permits_upload_ = GoogleUpdateSettings::GetCollectStatsConsent(); |
| // Update session ID |
| session_id_ = CrashMetricsReporter::GetInstance()->IncrementMetric( |
| CrashMetricsReporter::SESSION_ID); |
| |
| base::StatisticsRecorder::Initialize(); |
| CrashMetricsReporter::GetInstance()->set_active(true); |
| } |
| |
| // static |
| void MetricsService::Start() { |
| base::AutoLock lock(metrics_service_lock_); |
| |
| if (GetInstance()->state_ == ACTIVE) |
| return; |
| |
| GetInstance()->InitializeMetricsState(); |
| GetInstance()->SetRecording(true); |
| GetInstance()->SetReporting(true); |
| } |
| |
| // static |
| void MetricsService::Stop() { |
| base::AutoLock lock(metrics_service_lock_); |
| |
| GetInstance()->SetReporting(false); |
| GetInstance()->SetRecording(false); |
| } |
| |
| void MetricsService::SetRecording(bool enabled) { |
| DCHECK_EQ(thread_, base::PlatformThread::CurrentId()); |
| if (enabled == recording_active_) |
| return; |
| |
| if (enabled) { |
| StartRecording(); |
| } else { |
| state_ = STOPPED; |
| } |
| recording_active_ = enabled; |
| } |
| |
| // static |
| const std::string& MetricsService::GetClientID() { |
| // TODO(robertshield): Chrome Frame shouldn't generate a new ID on every run |
| // as this apparently breaks some assumptions during metric analysis. |
| // See http://crbug.com/117188 |
| if (client_id_.empty()) { |
| const int kGUIDSize = 39; |
| |
| GUID guid; |
| HRESULT guid_result = CoCreateGuid(&guid); |
| DCHECK(SUCCEEDED(guid_result)); |
| |
| string16 guid_string; |
| int result = StringFromGUID2(guid, |
| WriteInto(&guid_string, kGUIDSize), kGUIDSize); |
| DCHECK(result == kGUIDSize); |
| client_id_ = WideToUTF8(guid_string.substr(1, guid_string.length() - 2)); |
| } |
| return client_id_; |
| } |
| |
| // static |
| void CALLBACK MetricsService::TransmissionTimerProc(HWND window, |
| unsigned int message, |
| unsigned int event_id, |
| unsigned int time) { |
| DVLOG(1) << "Transmission timer notified"; |
| DCHECK(GetInstance() != NULL); |
| GetInstance()->UploadData(); |
| if (GetInstance()->initial_uma_upload_) { |
| // If this is the first uma upload by this process then subsequent uma |
| // uploads should occur once every 10 minutes(default). |
| GetInstance()->initial_uma_upload_ = false; |
| DCHECK(GetInstance()->transmission_timer_id_ != 0); |
| SetTimer(NULL, GetInstance()->transmission_timer_id_, |
| kMinMilliSecondsPerUMAUpload, |
| reinterpret_cast<TIMERPROC>(TransmissionTimerProc)); |
| } |
| } |
| |
| void MetricsService::SetReporting(bool enable) { |
| static const int kChromeFrameMetricsTimerId = 0xFFFFFFFF; |
| |
| DCHECK_EQ(thread_, base::PlatformThread::CurrentId()); |
| if (reporting_active_ != enable) { |
| reporting_active_ = enable; |
| if (reporting_active_) { |
| transmission_timer_id_ = |
| SetTimer(NULL, kChromeFrameMetricsTimerId, |
| kInitialUMAUploadTimeoutMilliSeconds, |
| reinterpret_cast<TIMERPROC>(TransmissionTimerProc)); |
| } else { |
| UploadData(); |
| } |
| } |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Recording control methods |
| |
| void MetricsService::StartRecording() { |
| DCHECK_EQ(thread_, base::PlatformThread::CurrentId()); |
| if (log_manager_.current_log()) |
| return; |
| |
| MetricsLogBase::LogType log_type = (state_ == INITIALIZED) ? |
| MetricsLogBase::INITIAL_LOG : MetricsLogBase::ONGOING_LOG; |
| log_manager_.BeginLoggingWithLog(new MetricsLogBase(GetClientID(), |
| session_id_, |
| GetVersionString()), |
| log_type); |
| if (state_ == INITIALIZED) |
| state_ = ACTIVE; |
| } |
| |
| void MetricsService::StopRecording(bool save_log) { |
| DCHECK_EQ(thread_, base::PlatformThread::CurrentId()); |
| if (!log_manager_.current_log()) |
| return; |
| |
| // Put incremental histogram deltas at the end of all log transmissions. |
| // Don't bother if we're going to discard current_log. |
| if (save_log) { |
| CrashMetricsReporter::GetInstance()->RecordCrashMetrics(); |
| RecordCurrentHistograms(); |
| } |
| |
| if (save_log) { |
| log_manager_.FinishCurrentLog(); |
| log_manager_.StageNextLogForUpload(); |
| } else { |
| log_manager_.DiscardCurrentLog(); |
| } |
| } |
| |
| void MetricsService::MakePendingLog() { |
| DCHECK_EQ(thread_, base::PlatformThread::CurrentId()); |
| if (log_manager_.has_staged_log()) |
| return; |
| |
| if (state_ != ACTIVE) { |
| NOTREACHED(); |
| return; |
| } |
| |
| StopRecording(true); |
| StartRecording(); |
| } |
| |
| bool MetricsService::TransmissionPermitted() const { |
| // If the user forbids uploading that's their business, and we don't upload |
| // anything. |
| return user_permits_upload_; |
| } |
| |
| bool MetricsService::UploadData() { |
| DCHECK_EQ(thread_, base::PlatformThread::CurrentId()); |
| |
| if (!GetInstance()->TransmissionPermitted()) |
| return false; |
| |
| static long currently_uploading = 0; |
| if (InterlockedCompareExchange(¤tly_uploading, 1, 0)) { |
| DVLOG(1) << "Contention for uploading metrics data. Backing off"; |
| return false; |
| } |
| |
| MakePendingLog(); |
| |
| bool ret = true; |
| |
| if (log_manager_.has_staged_log()) { |
| HRESULT hr = ChromeFrameMetricsDataUploader::UploadDataHelper( |
| log_manager_.staged_log_text(), kServerUrl, kMimeType); |
| DCHECK(SUCCEEDED(hr)); |
| log_manager_.DiscardStagedLog(); |
| } else { |
| NOTREACHED(); |
| ret = false; |
| } |
| |
| currently_uploading = 0; |
| return ret; |
| } |
| |
| // static |
| std::string MetricsService::GetVersionString() { |
| chrome::VersionInfo version_info; |
| if (version_info.is_valid()) { |
| std::string version = version_info.Version(); |
| // Add the -F extensions to ensure that UMA data uploaded by ChromeFrame |
| // lands in the ChromeFrame bucket. |
| version += "-F"; |
| if (!version_info.IsOfficialBuild()) |
| version.append("-devel"); |
| return version; |
| } else { |
| NOTREACHED() << "Unable to retrieve version string."; |
| } |
| |
| return std::string(); |
| } |