1/*
2 * Copyright (C) 2017 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "PerformanceMonitor.h"
28
29#include "Chrome.h"
30#include "ChromeClient.h"
31#include "DeprecatedGlobalSettings.h"
32#include "DiagnosticLoggingClient.h"
33#include "DiagnosticLoggingKeys.h"
34#include "Frame.h"
35#include "Logging.h"
36#include "Page.h"
37#include "PerformanceLogging.h"
38#include "RegistrableDomain.h"
39
40namespace WebCore {
41
42#define RELEASE_LOG_IF_ALLOWED(channel, fmt, ...) RELEASE_LOG_IF(m_page.isAlwaysOnLoggingAllowed(), channel, "%p - PerformanceMonitor::" fmt, this, ##__VA_ARGS__)
43
44static const Seconds cpuUsageMeasurementDelay { 5_s };
45static const Seconds postLoadCPUUsageMeasurementDuration { 10_s };
46static const Seconds backgroundCPUUsageMeasurementDuration { 5_min };
47static const Seconds cpuUsageSamplingInterval { 10_min };
48
49static const Seconds memoryUsageMeasurementDelay { 10_s };
50
51static const Seconds delayBeforeProcessMayBecomeInactive { 8_min };
52
53static const double postPageLoadCPUUsageDomainReportingThreshold { 20.0 }; // Reporting pages using over 20% CPU is roughly equivalent to reporting the 10% worst pages.
54#if !PLATFORM(IOS_FAMILY)
55static const uint64_t postPageLoadMemoryUsageDomainReportingThreshold { 2048 * MB };
56#endif
57
58static inline ActivityStateForCPUSampling activityStateForCPUSampling(OptionSet<ActivityState::Flag> state)
59{
60 if (!(state & ActivityState::IsVisible))
61 return ActivityStateForCPUSampling::NonVisible;
62 if (state & ActivityState::WindowIsActive)
63 return ActivityStateForCPUSampling::VisibleAndActive;
64 return ActivityStateForCPUSampling::VisibleNonActive;
65}
66
67PerformanceMonitor::PerformanceMonitor(Page& page)
68 : m_page(page)
69 , m_postPageLoadCPUUsageTimer(*this, &PerformanceMonitor::measurePostLoadCPUUsage)
70 , m_postBackgroundingCPUUsageTimer(*this, &PerformanceMonitor::measurePostBackgroundingCPUUsage)
71 , m_perActivityStateCPUUsageTimer(*this, &PerformanceMonitor::measurePerActivityStateCPUUsage)
72 , m_postPageLoadMemoryUsageTimer(*this, &PerformanceMonitor::measurePostLoadMemoryUsage)
73 , m_postBackgroundingMemoryUsageTimer(*this, &PerformanceMonitor::measurePostBackgroundingMemoryUsage)
74 , m_processMayBecomeInactiveTimer(*this, &PerformanceMonitor::processMayBecomeInactiveTimerFired)
75{
76 ASSERT(!page.isUtilityPage());
77
78 if (DeprecatedGlobalSettings::isPerActivityStateCPUUsageMeasurementEnabled()) {
79 m_perActivityStateCPUTime = CPUTime::get();
80 m_perActivityStateCPUUsageTimer.startRepeating(cpuUsageSamplingInterval);
81 }
82}
83
84void PerformanceMonitor::didStartProvisionalLoad()
85{
86 m_postLoadCPUTime = WTF::nullopt;
87 m_postPageLoadCPUUsageTimer.stop();
88 m_postPageLoadMemoryUsageTimer.stop();
89}
90
91void PerformanceMonitor::didFinishLoad()
92{
93 // Only do post-load CPU usage measurement if there is a single Page in the process in order to reduce noise.
94 if (DeprecatedGlobalSettings::isPostLoadCPUUsageMeasurementEnabled() && m_page.isOnlyNonUtilityPage()) {
95 m_postLoadCPUTime = WTF::nullopt;
96 m_postPageLoadCPUUsageTimer.startOneShot(cpuUsageMeasurementDelay);
97 }
98
99 // Likewise for post-load memory usage measurement.
100 if (DeprecatedGlobalSettings::isPostLoadMemoryUsageMeasurementEnabled() && m_page.isOnlyNonUtilityPage())
101 m_postPageLoadMemoryUsageTimer.startOneShot(memoryUsageMeasurementDelay);
102}
103
104void PerformanceMonitor::activityStateChanged(OptionSet<ActivityState::Flag> oldState, OptionSet<ActivityState::Flag> newState)
105{
106 auto changed = oldState ^ newState;
107 bool visibilityChanged = changed.contains(ActivityState::IsVisible);
108
109 // Measure CPU usage of pages when they are no longer visible.
110 if (DeprecatedGlobalSettings::isPostBackgroundingCPUUsageMeasurementEnabled() && visibilityChanged) {
111 m_postBackgroundingCPUTime = WTF::nullopt;
112 if (newState & ActivityState::IsVisible)
113 m_postBackgroundingCPUUsageTimer.stop();
114 else if (m_page.isOnlyNonUtilityPage())
115 m_postBackgroundingCPUUsageTimer.startOneShot(cpuUsageMeasurementDelay);
116 }
117
118 if (DeprecatedGlobalSettings::isPerActivityStateCPUUsageMeasurementEnabled()) {
119 // If visibility changed then report CPU usage right away because CPU usage is connected to visibility state.
120 auto oldActivityStateForCPUSampling = activityStateForCPUSampling(oldState);
121 if (oldActivityStateForCPUSampling != activityStateForCPUSampling(newState)) {
122 measureCPUUsageInActivityState(oldActivityStateForCPUSampling);
123 m_perActivityStateCPUUsageTimer.startRepeating(cpuUsageSamplingInterval);
124 }
125 }
126
127 if (DeprecatedGlobalSettings::isPostBackgroundingMemoryUsageMeasurementEnabled() && visibilityChanged) {
128 if (newState & ActivityState::IsVisible)
129 m_postBackgroundingMemoryUsageTimer.stop();
130 else if (m_page.isOnlyNonUtilityPage())
131 m_postBackgroundingMemoryUsageTimer.startOneShot(memoryUsageMeasurementDelay);
132 }
133
134 if (newState.containsAll({ ActivityState::IsVisible, ActivityState::WindowIsActive })) {
135 m_processMayBecomeInactive = false;
136 m_processMayBecomeInactiveTimer.stop();
137 } else if (!m_processMayBecomeInactive && !m_processMayBecomeInactiveTimer.isActive())
138 m_processMayBecomeInactiveTimer.startOneShot(delayBeforeProcessMayBecomeInactive);
139
140 updateProcessStateForMemoryPressure();
141}
142
143enum class ReportingReason { HighCPUUsage, HighMemoryUsage };
144static void reportPageOverPostLoadResourceThreshold(Page& page, ReportingReason reason)
145{
146#if ENABLE(PUBLIC_SUFFIX_LIST)
147 auto* document = page.mainFrame().document();
148 if (!document)
149 return;
150
151 RegistrableDomain registrableDomain { document->url() };
152 if (registrableDomain.isEmpty())
153 return;
154
155 switch (reason) {
156 case ReportingReason::HighCPUUsage:
157 page.diagnosticLoggingClient().logDiagnosticMessageWithEnhancedPrivacy(DiagnosticLoggingKeys::domainCausingEnergyDrainKey(), registrableDomain.string(), ShouldSample::No);
158 break;
159 case ReportingReason::HighMemoryUsage:
160 page.diagnosticLoggingClient().logDiagnosticMessageWithEnhancedPrivacy(DiagnosticLoggingKeys::domainCausingJetsamKey(), registrableDomain.string(), ShouldSample::No);
161 break;
162 }
163#else
164 UNUSED_PARAM(page);
165 UNUSED_PARAM(reason);
166#endif
167}
168
169void PerformanceMonitor::measurePostLoadCPUUsage()
170{
171 if (!m_page.isOnlyNonUtilityPage()) {
172 m_postLoadCPUTime = WTF::nullopt;
173 return;
174 }
175
176 if (!m_postLoadCPUTime) {
177 m_postLoadCPUTime = CPUTime::get();
178 if (m_postLoadCPUTime)
179 m_postPageLoadCPUUsageTimer.startOneShot(postLoadCPUUsageMeasurementDuration);
180 return;
181 }
182 Optional<CPUTime> cpuTime = CPUTime::get();
183 if (!cpuTime)
184 return;
185
186 double cpuUsage = cpuTime.value().percentageCPUUsageSince(*m_postLoadCPUTime);
187 RELEASE_LOG_IF_ALLOWED(PerformanceLogging, "measurePostLoadCPUUsage: Process was using %.1f%% CPU after the page load.", cpuUsage);
188 m_page.diagnosticLoggingClient().logDiagnosticMessage(DiagnosticLoggingKeys::postPageLoadCPUUsageKey(), DiagnosticLoggingKeys::foregroundCPUUsageToDiagnosticLoggingKey(cpuUsage), ShouldSample::No);
189
190 if (cpuUsage > postPageLoadCPUUsageDomainReportingThreshold)
191 reportPageOverPostLoadResourceThreshold(m_page, ReportingReason::HighCPUUsage);
192}
193
194void PerformanceMonitor::measurePostLoadMemoryUsage()
195{
196 if (!m_page.isOnlyNonUtilityPage())
197 return;
198
199 Optional<uint64_t> memoryUsage = PerformanceLogging::physicalFootprint();
200 if (!memoryUsage)
201 return;
202
203 RELEASE_LOG_IF_ALLOWED(PerformanceLogging, "measurePostLoadMemoryUsage: Process was using %llu bytes of memory after the page load.", memoryUsage.value());
204 m_page.diagnosticLoggingClient().logDiagnosticMessage(DiagnosticLoggingKeys::postPageLoadMemoryUsageKey(), DiagnosticLoggingKeys::memoryUsageToDiagnosticLoggingKey(memoryUsage.value()), ShouldSample::No);
205
206 // On iOS, we report actual Jetsams instead.
207#if !PLATFORM(IOS_FAMILY)
208 if (memoryUsage.value() > postPageLoadMemoryUsageDomainReportingThreshold)
209 reportPageOverPostLoadResourceThreshold(m_page, ReportingReason::HighMemoryUsage);
210#endif
211}
212
213void PerformanceMonitor::measurePostBackgroundingMemoryUsage()
214{
215 if (!m_page.isOnlyNonUtilityPage())
216 return;
217
218 Optional<uint64_t> memoryUsage = PerformanceLogging::physicalFootprint();
219 if (!memoryUsage)
220 return;
221
222 RELEASE_LOG_IF_ALLOWED(PerformanceLogging, "measurePostBackgroundingMemoryUsage: Process was using %llu bytes of memory after becoming non visible.", memoryUsage.value());
223 m_page.diagnosticLoggingClient().logDiagnosticMessage(DiagnosticLoggingKeys::postPageBackgroundingMemoryUsageKey(), DiagnosticLoggingKeys::memoryUsageToDiagnosticLoggingKey(memoryUsage.value()), ShouldSample::No);
224}
225
226void PerformanceMonitor::measurePostBackgroundingCPUUsage()
227{
228 if (!m_page.isOnlyNonUtilityPage()) {
229 m_postBackgroundingCPUTime = WTF::nullopt;
230 return;
231 }
232
233 if (!m_postBackgroundingCPUTime) {
234 m_postBackgroundingCPUTime = CPUTime::get();
235 if (m_postBackgroundingCPUTime)
236 m_postBackgroundingCPUUsageTimer.startOneShot(backgroundCPUUsageMeasurementDuration);
237 return;
238 }
239 Optional<CPUTime> cpuTime = CPUTime::get();
240 if (!cpuTime)
241 return;
242
243 double cpuUsage = cpuTime.value().percentageCPUUsageSince(*m_postBackgroundingCPUTime);
244 RELEASE_LOG_IF_ALLOWED(PerformanceLogging, "measurePostBackgroundingCPUUsage: Process was using %.1f%% CPU after becoming non visible.", cpuUsage);
245 m_page.diagnosticLoggingClient().logDiagnosticMessage(DiagnosticLoggingKeys::postPageBackgroundingCPUUsageKey(), DiagnosticLoggingKeys::backgroundCPUUsageToDiagnosticLoggingKey(cpuUsage), ShouldSample::No);
246}
247
248void PerformanceMonitor::measurePerActivityStateCPUUsage()
249{
250 measureCPUUsageInActivityState(activityStateForCPUSampling(m_page.activityState()));
251}
252
253#if !RELEASE_LOG_DISABLED
254
255static inline const char* stringForCPUSamplingActivityState(ActivityStateForCPUSampling activityState)
256{
257 switch (activityState) {
258 case ActivityStateForCPUSampling::NonVisible:
259 return "NonVisible";
260 case ActivityStateForCPUSampling::VisibleNonActive:
261 return "VisibleNonActive";
262 case ActivityStateForCPUSampling::VisibleAndActive:
263 return "VisibleAndActive";
264 }
265}
266
267#endif
268
269void PerformanceMonitor::measureCPUUsageInActivityState(ActivityStateForCPUSampling activityState)
270{
271 if (!m_page.isOnlyNonUtilityPage()) {
272 m_perActivityStateCPUTime = WTF::nullopt;
273 return;
274 }
275
276 if (!m_perActivityStateCPUTime) {
277 m_perActivityStateCPUTime = CPUTime::get();
278 return;
279 }
280
281 Optional<CPUTime> cpuTime = CPUTime::get();
282 if (!cpuTime) {
283 m_perActivityStateCPUTime = WTF::nullopt;
284 return;
285 }
286
287#if !RELEASE_LOG_DISABLED
288 double cpuUsage = cpuTime.value().percentageCPUUsageSince(*m_perActivityStateCPUTime);
289 RELEASE_LOG_IF_ALLOWED(PerformanceLogging, "measureCPUUsageInActivityState: Process is using %.1f%% CPU in state: %s", cpuUsage, stringForCPUSamplingActivityState(activityState));
290#endif
291 m_page.chrome().client().reportProcessCPUTime((cpuTime.value().systemTime + cpuTime.value().userTime) - (m_perActivityStateCPUTime.value().systemTime + m_perActivityStateCPUTime.value().userTime), activityState);
292
293 m_perActivityStateCPUTime = WTFMove(cpuTime);
294}
295
296void PerformanceMonitor::processMayBecomeInactiveTimerFired()
297{
298 m_processMayBecomeInactive = true;
299 updateProcessStateForMemoryPressure();
300}
301
302void PerformanceMonitor::updateProcessStateForMemoryPressure()
303{
304 bool hasAudiblePages = false;
305 bool hasCapturingPages = false;
306 bool mayBecomeInactive = true;
307
308 Page::forEachPage([&] (Page& page) {
309 if (!page.performanceMonitor())
310 return;
311 if (!page.performanceMonitor()->m_processMayBecomeInactive)
312 mayBecomeInactive = false;
313 if (page.activityState() & ActivityState::IsAudible)
314 hasAudiblePages = true;
315 if (page.activityState() & ActivityState::IsCapturingMedia)
316 hasCapturingPages = true;
317 });
318
319 bool isActiveProcess = !mayBecomeInactive || hasAudiblePages || hasCapturingPages;
320 MemoryPressureHandler::singleton().setProcessState(isActiveProcess ? WebsamProcessState::Active : WebsamProcessState::Inactive);
321}
322
323} // namespace WebCore
324