| // Copyright 2016 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. |
| |
| #include "content/browser/xr/metrics/session_metrics_helper.h" |
| |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "content/browser/xr/metrics/session_timer.h" |
| #include "content/browser/xr/metrics/webxr_session_tracker.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "device/vr/public/cpp/session_mode.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| const void* const kSessionMetricsHelperDataKey = &kSessionMetricsHelperDataKey; |
| |
| // minimum duration: 7 seconds for video, no minimum for headset/vr modes |
| // maximum gap: 7 seconds between videos. no gap for headset/vr-modes |
| constexpr base::TimeDelta kMinimumVideoSessionDuration( |
| base::TimeDelta::FromSecondsD(7)); |
| constexpr base::TimeDelta kMaximumVideoSessionGap( |
| base::TimeDelta::FromSecondsD(7)); |
| |
| constexpr base::TimeDelta kMinimumHeadsetSessionDuration( |
| base::TimeDelta::FromSecondsD(0)); |
| constexpr base::TimeDelta kMaximumHeadsetSessionGap( |
| base::TimeDelta::FromSecondsD(0)); |
| |
| // Handles the lifetime of the helper which is attached to a WebContents. |
| class SessionMetricsHelperData : public base::SupportsUserData::Data { |
| public: |
| explicit SessionMetricsHelperData( |
| SessionMetricsHelper* session_metrics_helper) |
| : session_metrics_helper_(session_metrics_helper) {} |
| |
| ~SessionMetricsHelperData() override { delete session_metrics_helper_; } |
| |
| SessionMetricsHelper* get() const { return session_metrics_helper_; } |
| |
| private: |
| SessionMetricsHelper* session_metrics_helper_; |
| |
| DISALLOW_IMPLICIT_CONSTRUCTORS(SessionMetricsHelperData); |
| }; |
| |
| // Helper method to log out both the mode and the initially requested features |
| // for a WebXRSessionTracker. WebXRSessionTracker is an unowned pointer. |
| void ReportInitialSessionData( |
| WebXRSessionTracker* webxr_session_tracker, |
| const device::mojom::XRSessionOptions& session_options, |
| const std::unordered_set<device::mojom::XRSessionFeature>& |
| enabled_features) { |
| DCHECK(webxr_session_tracker); |
| |
| webxr_session_tracker->ukm_entry()->SetMode( |
| static_cast<int64_t>(session_options.mode)); |
| webxr_session_tracker->ReportRequestedFeatures(session_options, |
| enabled_features); |
| } |
| |
| } // namespace |
| |
| // static |
| SessionMetricsHelper* SessionMetricsHelper::FromWebContents( |
| content::WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!web_contents) |
| return nullptr; |
| SessionMetricsHelperData* data = static_cast<SessionMetricsHelperData*>( |
| web_contents->GetUserData(kSessionMetricsHelperDataKey)); |
| return data ? data->get() : nullptr; |
| } |
| |
| // static |
| SessionMetricsHelper* SessionMetricsHelper::CreateForWebContents( |
| content::WebContents* contents) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // This is not leaked as the SessionMetricsHelperData will clean it up. |
| return new SessionMetricsHelper(contents); |
| } |
| |
| SessionMetricsHelper::SessionMetricsHelper(content::WebContents* contents) { |
| DVLOG(2) << __func__; |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(contents); |
| |
| num_videos_playing_ = contents->GetCurrentlyPlayingVideoCount(); |
| |
| Observe(contents); |
| contents->SetUserData(kSessionMetricsHelperDataKey, |
| std::make_unique<SessionMetricsHelperData>(this)); |
| } |
| |
| SessionMetricsHelper::~SessionMetricsHelper() { |
| DVLOG(2) << __func__; |
| } |
| |
| mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder> |
| SessionMetricsHelper::StartInlineSession( |
| const device::mojom::XRSessionOptions& session_options, |
| const std::unordered_set<device::mojom::XRSessionFeature>& enabled_features, |
| size_t session_id) { |
| DVLOG(1) << __func__; |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| DCHECK(webxr_inline_session_trackers_.find(session_id) == |
| webxr_inline_session_trackers_.end()); |
| |
| // TODO(crbug.com/1061899): The code here assumes that it's called on |
| // behalf of the active frame, which is not always true. |
| // Plumb explicit RenderFrameHost reference from VRSessionImpl. |
| auto result = webxr_inline_session_trackers_.emplace( |
| session_id, |
| std::make_unique<WebXRSessionTracker>( |
| std::make_unique<ukm::builders::XR_WebXR_Session>( |
| web_contents()->GetMainFrame()->GetPageUkmSourceId()))); |
| auto* tracker = result.first->second.get(); |
| |
| ReportInitialSessionData(tracker, session_options, enabled_features); |
| |
| return tracker->BindMetricsRecorderPipe(); |
| } |
| |
| void SessionMetricsHelper::StopAndRecordInlineSession(size_t session_id) { |
| DVLOG(1) << __func__; |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| auto it = webxr_inline_session_trackers_.find(session_id); |
| |
| if (it == webxr_inline_session_trackers_.end()) |
| return; |
| |
| it->second->SetSessionEnd(base::Time::Now()); |
| it->second->ukm_entry()->SetDuration( |
| it->second->GetRoundedDurationInSeconds()); |
| it->second->RecordEntry(); |
| |
| webxr_inline_session_trackers_.erase(it); |
| } |
| |
| mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder> |
| SessionMetricsHelper::StartImmersiveSession( |
| const device::mojom::XRSessionOptions& session_options, |
| const std::unordered_set<device::mojom::XRSessionFeature>& |
| enabled_features) { |
| DVLOG(1) << __func__; |
| DCHECK(!webxr_immersive_session_tracker_); |
| base::Time start_time = base::Time::Now(); |
| |
| // TODO(crbug.com/1061899): The code here assumes that it's called on |
| // behalf of the active frame, which is not always true. |
| // Plumb explicit RenderFrameHost reference from VRSessionImpl. |
| webxr_immersive_session_tracker_ = std::make_unique<WebXRSessionTracker>( |
| std::make_unique<ukm::builders::XR_WebXR_Session>( |
| web_contents()->GetMainFrame()->GetPageUkmSourceId())); |
| |
| // TODO(https://crbug.com/1056930): Consider renaming the timers to something |
| // that indicates both that these also record AR, and that these are no longer |
| // "suffixed" histograms. |
| session_timer_ = std::make_unique<SessionTimer>( |
| "VRSessionTime.WebVR", kMaximumHeadsetSessionGap, |
| kMinimumHeadsetSessionDuration); |
| session_timer_->StartSession(start_time); |
| |
| session_video_timer_ = std::make_unique<SessionTimer>( |
| "VRSessionVideoTime.WebVR", kMaximumVideoSessionGap, |
| kMinimumVideoSessionDuration); |
| |
| num_session_video_playback_ = num_videos_playing_; |
| |
| if (num_videos_playing_ > 0) { |
| session_video_timer_->StartSession(start_time); |
| } |
| |
| ReportInitialSessionData(webxr_immersive_session_tracker_.get(), |
| session_options, enabled_features); |
| |
| return webxr_immersive_session_tracker_->BindMetricsRecorderPipe(); |
| } |
| |
| void SessionMetricsHelper::StopAndRecordImmersiveSession() { |
| DVLOG(1) << __func__; |
| // A session cannot outlive a navigation, so we terminate it here. However, |
| // depending on how the session is torn down, we may be notified in any order |
| // of the navigation and then shutdown. If we don't have an active session, |
| // assume it's been stopped already and just return early. |
| if (!webxr_immersive_session_tracker_) { |
| return; |
| } |
| |
| webxr_immersive_session_tracker_->SetSessionEnd(base::Time::Now()); |
| webxr_immersive_session_tracker_->ukm_entry()->SetDuration( |
| webxr_immersive_session_tracker_->GetRoundedDurationInSeconds()); |
| webxr_immersive_session_tracker_->RecordEntry(); |
| webxr_immersive_session_tracker_ = nullptr; |
| |
| // Destroyig the timers will both stop the session and force them to log their |
| // metrics. |
| session_timer_ = nullptr; |
| session_video_timer_ = nullptr; |
| |
| UMA_HISTOGRAM_COUNTS_100("VRSessionVideoCount", num_session_video_playback_); |
| } |
| |
| void SessionMetricsHelper::MediaStartedPlaying( |
| const MediaPlayerInfo& media_info, |
| const content::MediaPlayerId&) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!media_info.has_video) |
| return; |
| |
| if (num_videos_playing_ == 0) { |
| // started playing video - start sessions |
| base::Time start_time = base::Time::Now(); |
| |
| if (session_video_timer_) { |
| session_video_timer_->StartSession(start_time); |
| } |
| } |
| |
| num_videos_playing_++; |
| num_session_video_playback_++; |
| } |
| |
| void SessionMetricsHelper::MediaStoppedPlaying( |
| const MediaPlayerInfo& media_info, |
| const content::MediaPlayerId&, |
| WebContentsObserver::MediaStoppedReason reason) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (!media_info.has_video) |
| return; |
| |
| num_videos_playing_--; |
| |
| if (num_videos_playing_ == 0) { |
| // stopped playing video - update existing video sessions |
| base::Time stop_time = base::Time::Now(); |
| |
| if (session_video_timer_) { |
| session_video_timer_->StopSession(true, stop_time); |
| } |
| } |
| } |
| |
| void SessionMetricsHelper::DidStartNavigation( |
| content::NavigationHandle* handle) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (handle && handle->IsInMainFrame() && !handle->IsSameDocument()) { |
| // All sessions are terminated on navigations, so to ensure that we log |
| // everything that we have, cleanup any outstanding session trackers now. |
| if (webxr_immersive_session_tracker_) { |
| StopAndRecordImmersiveSession(); |
| } |
| |
| for (auto& inline_session_tracker : webxr_inline_session_trackers_) { |
| inline_session_tracker.second->SetSessionEnd(base::Time::Now()); |
| inline_session_tracker.second->ukm_entry()->SetDuration( |
| inline_session_tracker.second->GetRoundedDurationInSeconds()); |
| inline_session_tracker.second->RecordEntry(); |
| } |
| |
| webxr_inline_session_trackers_.clear(); |
| } |
| } |
| |
| } // namespace content |