Drop unused ResourceLoadStatistics members
[WebKit-https.git] / Source / WebCore / loader / ResourceLoadObserver.cpp
1 /*
2  * Copyright (C) 2016-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 "ResourceLoadObserver.h"
28
29 #include "Document.h"
30 #include "Frame.h"
31 #include "Logging.h"
32 #include "MainFrame.h"
33 #include "Page.h"
34 #include "ResourceLoadStatistics.h"
35 #include "ResourceRequest.h"
36 #include "ResourceResponse.h"
37 #include "SecurityOrigin.h"
38 #include "Settings.h"
39 #include "URL.h"
40
41 namespace WebCore {
42
43 template<typename T> static inline String primaryDomain(const T& value)
44 {
45     return ResourceLoadStatistics::primaryDomain(value);
46 }
47
48 static Seconds timestampResolution { 1_h };
49 static const Seconds minimumNotificationInterval { 5_s };
50
51 ResourceLoadObserver& ResourceLoadObserver::shared()
52 {
53     static NeverDestroyed<ResourceLoadObserver> resourceLoadObserver;
54     return resourceLoadObserver;
55 }
56
57 void ResourceLoadObserver::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
58 {
59     ASSERT(!m_notificationCallback);
60     m_notificationCallback = WTFMove(notificationCallback);
61 }
62
63 ResourceLoadObserver::ResourceLoadObserver()
64     : m_notificationTimer(*this, &ResourceLoadObserver::notificationTimerFired)
65 {
66 }
67
68 static inline bool is3xxRedirect(const ResourceResponse& response)
69 {
70     return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
71 }
72
73 bool ResourceLoadObserver::shouldLog(Page* page) const
74 {
75     // FIXME: Err on the safe side until we have sorted out what to do in worker contexts
76     if (!page)
77         return false;
78
79     return Settings::resourceLoadStatisticsEnabled() && !page->usesEphemeralSession() && m_notificationCallback;
80 }
81
82 void ResourceLoadObserver::logFrameNavigation(const Frame& frame, const Frame& topFrame, const ResourceRequest& newRequest)
83 {
84     ASSERT(frame.document());
85     ASSERT(topFrame.document());
86     ASSERT(topFrame.page());
87     
88     if (!shouldLog(topFrame.page()))
89         return;
90
91     auto& sourceURL = frame.document()->url();
92     auto& targetURL = newRequest.url();
93     auto& mainFrameURL = topFrame.document()->url();
94     
95     if (!targetURL.isValid() || !mainFrameURL.isValid())
96         return;
97
98     auto targetHost = targetURL.host();
99     auto mainFrameHost = mainFrameURL.host();
100
101     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || targetHost == sourceURL.host())
102         return;
103
104     auto targetPrimaryDomain = primaryDomain(targetURL);
105     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
106     auto sourcePrimaryDomain = primaryDomain(sourceURL);
107     
108     if (targetPrimaryDomain == mainFramePrimaryDomain || targetPrimaryDomain == sourcePrimaryDomain)
109         return;
110
111     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
112
113     // Always fire if we have previously removed data records for this domain
114     // FIXME: targetStatistics.dataRecordsRemoved is always 0 in WebCore as it gets populated in the UIProcess.
115     bool shouldCallNotificationCallback = targetStatistics.dataRecordsRemoved > 0;
116
117     if (!frame.isMainFrame()) {
118         auto subframeUnderTopFrameOriginsResult = targetStatistics.subframeUnderTopFrameOrigins.add(mainFramePrimaryDomain);
119         if (subframeUnderTopFrameOriginsResult.isNewEntry)
120             shouldCallNotificationCallback = true;
121     }
122
123     if (shouldCallNotificationCallback)
124         scheduleNotificationIfNeeded();
125 }
126     
127 void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
128 {
129     ASSERT(frame->page());
130
131     if (!shouldLog(frame->page()))
132         return;
133
134     bool isRedirect = is3xxRedirect(redirectResponse);
135     const URL& sourceURL = redirectResponse.url();
136     const URL& targetURL = newRequest.url();
137     const URL& mainFrameURL = frame ? frame->mainFrame().document()->url() : URL();
138     
139     auto targetHost = targetURL.host();
140     auto mainFrameHost = mainFrameURL.host();
141
142     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || (isRedirect && targetHost == sourceURL.host()))
143         return;
144
145     auto targetPrimaryDomain = primaryDomain(targetURL);
146     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
147     auto sourcePrimaryDomain = primaryDomain(sourceURL);
148     
149     if (targetPrimaryDomain == mainFramePrimaryDomain || (isRedirect && targetPrimaryDomain == sourcePrimaryDomain))
150         return;
151
152     bool shouldCallNotificationCallback = false;
153
154     {
155         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
156
157         // Always fire if we have previously removed data records for this domain
158         // FIXME: targetStatistics.dataRecordsRemoved is always 0 in WebCore as it gets populated in the UIProcess.
159         shouldCallNotificationCallback = targetStatistics.dataRecordsRemoved > 0;
160
161         auto subresourceUnderTopFrameOriginsResult = targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain);
162         if (subresourceUnderTopFrameOriginsResult.isNewEntry)
163             shouldCallNotificationCallback = true;
164     }
165
166     if (isRedirect) {
167         auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
168         auto subresourceUniqueRedirectsToResult = redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain);
169         if (subresourceUniqueRedirectsToResult.isNewEntry)
170             shouldCallNotificationCallback = true;
171     }
172
173     if (shouldCallNotificationCallback)
174         scheduleNotificationIfNeeded();
175 }
176
177 void ResourceLoadObserver::logWebSocketLoading(const Frame* frame, const URL& targetURL)
178 {
179     // FIXME: Web sockets can run in detached frames. Decide how to count such connections.
180     // See LayoutTests/http/tests/websocket/construct-in-detached-frame.html
181     if (!frame)
182         return;
183
184     if (!shouldLog(frame->page()))
185         return;
186
187     auto& mainFrameURL = frame->mainFrame().document()->url();
188
189     auto targetHost = targetURL.host();
190     auto mainFrameHost = mainFrameURL.host();
191     
192     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
193         return;
194     
195     auto targetPrimaryDomain = primaryDomain(targetURL);
196     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
197     
198     if (targetPrimaryDomain == mainFramePrimaryDomain)
199         return;
200
201     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
202
203     // Always fire if we have previously removed data records for this domain
204     // FIXME: targetStatistics.dataRecordsRemoved is always 0 in WebCore as it gets populated in the UIProcess.
205     bool shouldCallNotificationCallback = targetStatistics.dataRecordsRemoved > 0;
206
207     auto subresourceUnderTopFrameOriginsResult = targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain);
208     if (subresourceUnderTopFrameOriginsResult.isNewEntry)
209         shouldCallNotificationCallback = true;
210
211     if (shouldCallNotificationCallback)
212         scheduleNotificationIfNeeded();
213 }
214
215 static WallTime reduceTimeResolution(WallTime time)
216 {
217     return WallTime::fromRawSeconds(std::floor(time.secondsSinceEpoch() / timestampResolution) * timestampResolution.seconds());
218 }
219
220 void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
221 {
222     ASSERT(document.page());
223
224     if (!shouldLog(document.page()))
225         return;
226
227     auto& url = document.url();
228     if (url.isBlankURL() || url.isEmpty())
229         return;
230
231     auto& statistics = ensureResourceStatisticsForPrimaryDomain(primaryDomain(url));
232     auto newTime = reduceTimeResolution(WallTime::now());
233     if (newTime == statistics.mostRecentUserInteractionTime)
234         return;
235
236     statistics.hadUserInteraction = true;
237     statistics.mostRecentUserInteractionTime = newTime;
238
239     scheduleNotificationIfNeeded();
240 }
241
242 ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
243 {
244     auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
245         return ResourceLoadStatistics(primaryDomain);
246     });
247     return addResult.iterator->value;
248 }
249
250 void ResourceLoadObserver::scheduleNotificationIfNeeded()
251 {
252     ASSERT(m_notificationCallback);
253     if (m_resourceStatisticsMap.isEmpty()) {
254         m_notificationTimer.stop();
255         return;
256     }
257
258     if (!m_notificationTimer.isActive())
259         m_notificationTimer.startOneShot(minimumNotificationInterval);
260 }
261
262 void ResourceLoadObserver::notificationTimerFired()
263 {
264     ASSERT(m_notificationCallback);
265     m_notificationCallback(takeStatistics());
266 }
267
268 String ResourceLoadObserver::statisticsForOrigin(const String& origin)
269 {
270     auto iter = m_resourceStatisticsMap.find(origin);
271     if (iter == m_resourceStatisticsMap.end())
272         return emptyString();
273
274     return "Statistics for " + origin + ":\n" + iter->value.toString();
275 }
276
277 Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
278 {
279     Vector<ResourceLoadStatistics> statistics;
280     statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
281     for (auto& statistic : m_resourceStatisticsMap.values())
282         statistics.uncheckedAppend(WTFMove(statistic));
283
284     m_resourceStatisticsMap.clear();
285
286     return statistics;
287 }
288
289 } // namespace WebCore