1/*
2 * Copyright (C) 2007 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 COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "PageCache.h"
28
29#include "ApplicationCacheHost.h"
30#include "BackForwardController.h"
31#include "MemoryCache.h"
32#include "CachedPage.h"
33#include "DOMWindow.h"
34#include "DatabaseManager.h"
35#include "DeviceMotionController.h"
36#include "DeviceOrientationController.h"
37#include "Document.h"
38#include "DocumentLoader.h"
39#include "Frame.h"
40#include "FrameLoader.h"
41#include "FrameLoaderClient.h"
42#include "FrameLoaderStateMachine.h"
43#include "FrameView.h"
44#include "HistogramSupport.h"
45#include "HistoryController.h"
46#include "HistoryItem.h"
47#include "Logging.h"
48#include "Page.h"
49#include "Settings.h"
50#include "SharedWorkerRepository.h"
51#include <wtf/CurrentTime.h>
52#include <wtf/text/CString.h>
53#include <wtf/text/StringConcatenate.h>
54
55#if ENABLE(PROXIMITY_EVENTS)
56#include "DeviceProximityController.h"
57#endif
58
59using namespace std;
60
61namespace WebCore {
62
63#if !defined(NDEBUG)
64
65#define PCLOG(...) LOG(PageCache, "%*s%s", indentLevel*4, "", makeString(__VA_ARGS__).utf8().data())
66
67// Used in histograms, please only add at the end, and do not remove elements (renaming e.g. to "FooEnumUnused1" is fine).
68// This is because statistics may be gathered from histograms between versions over time, and re-using values causes collisions.
69enum ReasonFrameCannotBeInPageCache {
70    NoDocumentLoader = 0,
71    MainDocumentError,
72    IsErrorPage,
73    HasPlugins,
74    IsHttpsAndCacheControlled,
75    HasUnloadListener,
76    HasDatabaseHandles,
77    HasSharedWorkers,
78    NoHistoryItem,
79    QuickRedirectComing,
80    IsLoadingInAPISense,
81    IsStopping,
82    CannotSuspendActiveDOMObjects,
83    DocumentLoaderUsesApplicationCache,
84    ClientDeniesCaching,
85    NumberOfReasonsFramesCannotBeInPageCache,
86};
87COMPILE_ASSERT(NumberOfReasonsFramesCannotBeInPageCache <= sizeof(unsigned)*8, ReasonFrameCannotBeInPageCacheDoesNotFitInBitmap);
88
89static unsigned logCanCacheFrameDecision(Frame* frame, int indentLevel)
90{
91    PCLOG("+---");
92    if (!frame->loader()->documentLoader()) {
93        PCLOG("   -There is no DocumentLoader object");
94        return 1 << NoDocumentLoader;
95    }
96
97    KURL currentURL = frame->loader()->documentLoader()->url();
98    KURL newURL = frame->loader()->provisionalDocumentLoader() ? frame->loader()->provisionalDocumentLoader()->url() : KURL();
99    if (!newURL.isEmpty())
100        PCLOG(" Determining if frame can be cached navigating from (", currentURL.string(), ") to (", newURL.string(), "):");
101    else
102        PCLOG(" Determining if subframe with URL (", currentURL.string(), ") can be cached:");
103
104    unsigned rejectReasons = 0;
105    if (!frame->loader()->documentLoader()->mainDocumentError().isNull()) {
106        PCLOG("   -Main document has an error");
107        rejectReasons |= 1 << MainDocumentError;
108    }
109    if (frame->loader()->documentLoader()->substituteData().isValid() && frame->loader()->documentLoader()->substituteData().failingURL().isEmpty()) {
110        PCLOG("   -Frame is an error page");
111        rejectReasons |= 1 << IsErrorPage;
112    }
113    if (frame->loader()->subframeLoader()->containsPlugins() && !frame->page()->settings()->pageCacheSupportsPlugins()) {
114        PCLOG("   -Frame contains plugins");
115        rejectReasons |= 1 << HasPlugins;
116    }
117    if (frame->document()->url().protocolIs("https")
118        && (frame->loader()->documentLoader()->response().cacheControlContainsNoCache()
119            || frame->loader()->documentLoader()->response().cacheControlContainsNoStore())) {
120        PCLOG("   -Frame is HTTPS, and cache control prohibits caching or storing");
121        rejectReasons |= 1 << IsHttpsAndCacheControlled;
122    }
123    if (frame->document()->domWindow() && frame->document()->domWindow()->hasEventListeners(eventNames().unloadEvent)) {
124        PCLOG("   -Frame has an unload event listener");
125        rejectReasons |= 1 << HasUnloadListener;
126    }
127#if ENABLE(SQL_DATABASE)
128    if (DatabaseManager::manager().hasOpenDatabases(frame->document())) {
129        PCLOG("   -Frame has open database handles");
130        rejectReasons |= 1 << HasDatabaseHandles;
131    }
132#endif
133#if ENABLE(SHARED_WORKERS)
134    if (SharedWorkerRepository::hasSharedWorkers(frame->document())) {
135        PCLOG("   -Frame has associated SharedWorkers");
136        rejectReasons |= 1 << HasSharedWorkers;
137    }
138#endif
139    if (!frame->loader()->history()->currentItem()) {
140        PCLOG("   -No current history item");
141        rejectReasons |= 1 << NoHistoryItem;
142    }
143    if (frame->loader()->quickRedirectComing()) {
144        PCLOG("   -Quick redirect is coming");
145        rejectReasons |= 1 << QuickRedirectComing;
146    }
147    if (frame->loader()->documentLoader()->isLoadingInAPISense()) {
148        PCLOG("   -DocumentLoader is still loading in API sense");
149        rejectReasons |= 1 << IsLoadingInAPISense;
150    }
151    if (frame->loader()->documentLoader()->isStopping()) {
152        PCLOG("   -DocumentLoader is in the middle of stopping");
153        rejectReasons |= 1 << IsStopping;
154    }
155    if (!frame->document()->canSuspendActiveDOMObjects()) {
156        PCLOG("   -The document cannot suspect its active DOM Objects");
157        rejectReasons |= 1 << CannotSuspendActiveDOMObjects;
158    }
159    if (!frame->loader()->documentLoader()->applicationCacheHost()->canCacheInPageCache()) {
160        PCLOG("   -The DocumentLoader uses an application cache");
161        rejectReasons |= 1 << DocumentLoaderUsesApplicationCache;
162    }
163    if (!frame->loader()->client()->canCachePage()) {
164        PCLOG("   -The client says this frame cannot be cached");
165        rejectReasons |= 1 << ClientDeniesCaching;
166    }
167
168    HistogramSupport::histogramEnumeration("PageCache.FrameCacheable", !rejectReasons, 2);
169    int reasonCount = 0;
170    for (int i = 0; i < NumberOfReasonsFramesCannotBeInPageCache; ++i) {
171        if (rejectReasons & (1 << i)) {
172            ++reasonCount;
173            HistogramSupport::histogramEnumeration("PageCache.FrameRejectReason", i, NumberOfReasonsFramesCannotBeInPageCache);
174        }
175    }
176    HistogramSupport::histogramEnumeration("PageCache.FrameRejectReasonCount", reasonCount, 1 + NumberOfReasonsFramesCannotBeInPageCache);
177
178    for (Frame* child = frame->tree()->firstChild(); child; child = child->tree()->nextSibling())
179        rejectReasons |= logCanCacheFrameDecision(child, indentLevel + 1);
180
181    PCLOG(rejectReasons ? " Frame CANNOT be cached" : " Frame CAN be cached");
182    PCLOG("+---");
183
184    return rejectReasons;
185}
186
187// Used in histograms, please only add at the end, and do not remove elements (renaming e.g. to "FooEnumUnused1" is fine).
188// This is because statistics may be gathered from histograms between versions over time, and re-using values causes collisions.
189enum ReasonPageCannotBeInPageCache {
190    FrameCannotBeInPageCache = 0,
191    DisabledBackForwardList,
192    DisabledPageCache,
193    UsesDeviceMotion,
194    UsesDeviceOrientation,
195    IsReload,
196    IsReloadFromOrigin,
197    IsSameLoad,
198    NumberOfReasonsPagesCannotBeInPageCache,
199};
200COMPILE_ASSERT(NumberOfReasonsPagesCannotBeInPageCache <= sizeof(unsigned)*8, ReasonPageCannotBeInPageCacheDoesNotFitInBitmap);
201
202static void logCanCachePageDecision(Page* page)
203{
204    // Only bother logging for main frames that have actually loaded and have content.
205    if (page->mainFrame()->loader()->stateMachine()->creatingInitialEmptyDocument())
206        return;
207    KURL currentURL = page->mainFrame()->loader()->documentLoader() ? page->mainFrame()->loader()->documentLoader()->url() : KURL();
208    if (currentURL.isEmpty())
209        return;
210
211    int indentLevel = 0;
212    PCLOG("--------\n Determining if page can be cached:");
213
214    unsigned rejectReasons = 0;
215    unsigned frameRejectReasons = logCanCacheFrameDecision(page->mainFrame(), indentLevel+1);
216    if (frameRejectReasons)
217        rejectReasons |= 1 << FrameCannotBeInPageCache;
218
219    if (!page->backForward()->isActive()) {
220        PCLOG("   -The back/forward list is disabled or has 0 capacity");
221        rejectReasons |= 1 << DisabledBackForwardList;
222    }
223    if (!page->settings()->usesPageCache()) {
224        PCLOG("   -Page settings says b/f cache disabled");
225        rejectReasons |= 1 << DisabledPageCache;
226    }
227#if ENABLE(DEVICE_ORIENTATION)
228    if (DeviceMotionController::isActiveAt(page)) {
229        PCLOG("   -Page is using DeviceMotion");
230        rejectReasons |= 1 << UsesDeviceMotion;
231    }
232    if (DeviceOrientationController::isActiveAt(page)) {
233        PCLOG("   -Page is using DeviceOrientation");
234        rejectReasons |= 1 << UsesDeviceOrientation;
235    }
236#endif
237#if ENABLE(PROXIMITY_EVENTS)
238    if (DeviceProximityController::isActiveAt(page)) {
239        PCLOG("   -Page is using DeviceProximity");
240        rejectReasons |= 1 << UsesDeviceMotion;
241    }
242#endif
243    FrameLoadType loadType = page->mainFrame()->loader()->loadType();
244    if (loadType == FrameLoadTypeReload) {
245        PCLOG("   -Load type is: Reload");
246        rejectReasons |= 1 << IsReload;
247    }
248    if (loadType == FrameLoadTypeReloadFromOrigin) {
249        PCLOG("   -Load type is: Reload from origin");
250        rejectReasons |= 1 << IsReloadFromOrigin;
251    }
252    if (loadType == FrameLoadTypeSame) {
253        PCLOG("   -Load type is: Same");
254        rejectReasons |= 1 << IsSameLoad;
255    }
256
257    PCLOG(rejectReasons ? " Page CANNOT be cached\n--------" : " Page CAN be cached\n--------");
258
259    HistogramSupport::histogramEnumeration("PageCache.PageCacheable", !rejectReasons, 2);
260    int reasonCount = 0;
261    for (int i = 0; i < NumberOfReasonsPagesCannotBeInPageCache; ++i) {
262        if (rejectReasons & (1 << i)) {
263            ++reasonCount;
264            HistogramSupport::histogramEnumeration("PageCache.PageRejectReason", i, NumberOfReasonsPagesCannotBeInPageCache);
265        }
266    }
267    HistogramSupport::histogramEnumeration("PageCache.PageRejectReasonCount", reasonCount, 1 + NumberOfReasonsPagesCannotBeInPageCache);
268    const bool settingsDisabledPageCache = rejectReasons & (1 << DisabledPageCache);
269    HistogramSupport::histogramEnumeration("PageCache.PageRejectReasonCountExcludingSettings", reasonCount - settingsDisabledPageCache, NumberOfReasonsPagesCannotBeInPageCache);
270
271    // Report also on the frame reasons by page; this is distinct from the per frame statistics since it coalesces the
272    // causes from all subframes together.
273    HistogramSupport::histogramEnumeration("PageCache.FrameCacheableByPage", !frameRejectReasons, 2);
274    int frameReasonCount = 0;
275    for (int i = 0; i <= NumberOfReasonsFramesCannotBeInPageCache; ++i) {
276        if (frameRejectReasons & (1 << i)) {
277            ++frameReasonCount;
278            HistogramSupport::histogramEnumeration("PageCache.FrameRejectReasonByPage", i, NumberOfReasonsFramesCannotBeInPageCache);
279        }
280    }
281
282    HistogramSupport::histogramEnumeration("PageCache.FrameRejectReasonCountByPage", frameReasonCount, 1 + NumberOfReasonsFramesCannotBeInPageCache);
283}
284
285#endif // !defined(NDEBUG)
286
287PageCache* pageCache()
288{
289    static PageCache* staticPageCache = new PageCache;
290    return staticPageCache;
291}
292
293PageCache::PageCache()
294    : m_capacity(0)
295    , m_size(0)
296    , m_head(0)
297    , m_tail(0)
298#if USE(ACCELERATED_COMPOSITING)
299    , m_shouldClearBackingStores(false)
300#endif
301{
302}
303
304bool PageCache::canCachePageContainingThisFrame(Frame* frame)
305{
306    for (Frame* child = frame->tree()->firstChild(); child; child = child->tree()->nextSibling()) {
307        if (!canCachePageContainingThisFrame(child))
308            return false;
309    }
310
311    FrameLoader* frameLoader = frame->loader();
312    DocumentLoader* documentLoader = frameLoader->documentLoader();
313    Document* document = frame->document();
314
315    return documentLoader
316        && documentLoader->mainDocumentError().isNull()
317        // Do not cache error pages (these can be recognized as pages with substitute data or unreachable URLs).
318        && !(documentLoader->substituteData().isValid() && !documentLoader->substituteData().failingURL().isEmpty())
319        && (!frameLoader->subframeLoader()->containsPlugins() || frame->page()->settings()->pageCacheSupportsPlugins())
320        && (!document->url().protocolIs("https") || (!documentLoader->response().cacheControlContainsNoCache() && !documentLoader->response().cacheControlContainsNoStore()))
321        && (!document->domWindow() || !document->domWindow()->hasEventListeners(eventNames().unloadEvent))
322#if ENABLE(SQL_DATABASE)
323        && !DatabaseManager::manager().hasOpenDatabases(document)
324#endif
325#if ENABLE(SHARED_WORKERS)
326        && !SharedWorkerRepository::hasSharedWorkers(document)
327#endif
328        && frameLoader->history()->currentItem()
329        && !frameLoader->quickRedirectComing()
330        && !documentLoader->isLoadingInAPISense()
331        && !documentLoader->isStopping()
332        && document->canSuspendActiveDOMObjects()
333        // FIXME: We should investigating caching frames that have an associated
334        // application cache. <rdar://problem/5917899> tracks that work.
335        && documentLoader->applicationCacheHost()->canCacheInPageCache()
336        && frameLoader->client()->canCachePage();
337}
338
339bool PageCache::canCache(Page* page) const
340{
341    if (!page)
342        return false;
343
344#if !defined(NDEBUG)
345    logCanCachePageDecision(page);
346#endif
347
348    // Cache the page, if possible.
349    // Don't write to the cache if in the middle of a redirect, since we will want to
350    // store the final page we end up on.
351    // No point writing to the cache on a reload or loadSame, since we will just write
352    // over it again when we leave that page.
353    FrameLoadType loadType = page->mainFrame()->loader()->loadType();
354
355    return m_capacity > 0
356        && canCachePageContainingThisFrame(page->mainFrame())
357        && page->backForward()->isActive()
358        && page->settings()->usesPageCache()
359#if ENABLE(DEVICE_ORIENTATION)
360        && !DeviceMotionController::isActiveAt(page)
361        && !DeviceOrientationController::isActiveAt(page)
362#endif
363#if ENABLE(PROXIMITY_EVENTS)
364        && !DeviceProximityController::isActiveAt(page)
365#endif
366        && (loadType == FrameLoadTypeStandard
367            || loadType == FrameLoadTypeBack
368            || loadType == FrameLoadTypeForward
369            || loadType == FrameLoadTypeIndexedBackForward);
370}
371
372void PageCache::setCapacity(int capacity)
373{
374    ASSERT(capacity >= 0);
375    m_capacity = max(capacity, 0);
376
377    prune();
378}
379
380int PageCache::frameCount() const
381{
382    int frameCount = 0;
383    for (HistoryItem* current = m_head; current; current = current->m_next) {
384        ++frameCount;
385        ASSERT(current->m_cachedPage);
386        frameCount += current->m_cachedPage ? current->m_cachedPage->cachedMainFrame()->descendantFrameCount() : 0;
387    }
388
389    return frameCount;
390}
391
392void PageCache::markPagesForVistedLinkStyleRecalc()
393{
394    for (HistoryItem* current = m_head; current; current = current->m_next) {
395        if (current->m_cachedPage)
396            current->m_cachedPage->markForVistedLinkStyleRecalc();
397    }
398}
399
400void PageCache::markPagesForFullStyleRecalc(Page* page)
401{
402    Frame* mainFrame = page->mainFrame();
403
404    for (HistoryItem* current = m_head; current; current = current->m_next) {
405        CachedPage* cachedPage = current->m_cachedPage.get();
406        if (cachedPage && cachedPage->cachedMainFrame()->view()->frame() == mainFrame)
407            cachedPage->markForFullStyleRecalc();
408    }
409}
410
411
412#if USE(ACCELERATED_COMPOSITING)
413void PageCache::markPagesForDeviceScaleChanged(Page* page)
414{
415    Frame* mainFrame = page->mainFrame();
416
417    for (HistoryItem* current = m_head; current; current = current->m_next) {
418        CachedPage* cachedPage = current->m_cachedPage.get();
419        if (cachedPage && cachedPage->cachedMainFrame()->view()->frame() == mainFrame)
420            cachedPage->markForDeviceScaleChanged();
421    }
422}
423#endif
424
425#if ENABLE(VIDEO_TRACK)
426void PageCache::markPagesForCaptionPreferencesChanged()
427{
428    for (HistoryItem* current = m_head; current; current = current->m_next) {
429        if (current->m_cachedPage)
430            current->m_cachedPage->markForCaptionPreferencesChanged();
431    }
432}
433#endif
434
435void PageCache::add(PassRefPtr<HistoryItem> prpItem, Page* page)
436{
437    ASSERT(prpItem);
438    ASSERT(page);
439    ASSERT(canCache(page));
440
441    HistoryItem* item = prpItem.leakRef(); // Balanced in remove().
442
443    // Remove stale cache entry if necessary.
444    if (item->m_cachedPage)
445        remove(item);
446
447    item->m_cachedPage = CachedPage::create(page);
448    addToLRUList(item);
449    ++m_size;
450
451    prune();
452}
453
454CachedPage* PageCache::get(HistoryItem* item)
455{
456    if (!item)
457        return 0;
458
459    if (CachedPage* cachedPage = item->m_cachedPage.get()) {
460        if (!cachedPage->hasExpired())
461            return cachedPage;
462
463        LOG(PageCache, "Not restoring page for %s from back/forward cache because cache entry has expired", item->url().string().ascii().data());
464        pageCache()->remove(item);
465    }
466    return 0;
467}
468
469void PageCache::remove(HistoryItem* item)
470{
471    // Safely ignore attempts to remove items not in the cache.
472    if (!item || !item->m_cachedPage)
473        return;
474
475    item->m_cachedPage.clear();
476    removeFromLRUList(item);
477    --m_size;
478
479    item->deref(); // Balanced in add().
480}
481
482void PageCache::prune()
483{
484    while (m_size > m_capacity) {
485        ASSERT(m_tail && m_tail->m_cachedPage);
486        remove(m_tail);
487    }
488}
489
490void PageCache::addToLRUList(HistoryItem* item)
491{
492    item->m_next = m_head;
493    item->m_prev = 0;
494
495    if (m_head) {
496        ASSERT(m_tail);
497        m_head->m_prev = item;
498    } else {
499        ASSERT(!m_tail);
500        m_tail = item;
501    }
502
503    m_head = item;
504}
505
506void PageCache::removeFromLRUList(HistoryItem* item)
507{
508    if (!item->m_next) {
509        ASSERT(item == m_tail);
510        m_tail = item->m_prev;
511    } else {
512        ASSERT(item != m_tail);
513        item->m_next->m_prev = item->m_prev;
514    }
515
516    if (!item->m_prev) {
517        ASSERT(item == m_head);
518        m_head = item->m_next;
519    } else {
520        ASSERT(item != m_head);
521        item->m_prev->m_next = item->m_next;
522    }
523}
524
525} // namespace WebCore
526