Home | History | Annotate | Download | only in history
      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 "DeviceMotionController.h"
     35 #include "DeviceOrientationController.h"
     36 #include "Document.h"
     37 #include "DocumentLoader.h"
     38 #include "Frame.h"
     39 #include "FrameLoader.h"
     40 #include "FrameLoaderClient.h"
     41 #include "FrameLoaderStateMachine.h"
     42 #include "HistoryItem.h"
     43 #include "Logging.h"
     44 #include "Page.h"
     45 #include "Settings.h"
     46 #include "SharedWorkerRepository.h"
     47 #include "SystemTime.h"
     48 #include <wtf/CurrentTime.h>
     49 #include <wtf/text/CString.h>
     50 #include <wtf/text/StringConcatenate.h>
     51 
     52 using namespace std;
     53 
     54 namespace WebCore {
     55 
     56 static const double autoreleaseInterval = 3;
     57 
     58 #ifndef NDEBUG
     59 
     60 static String& pageCacheLogPrefix(int indentLevel)
     61 {
     62     static int previousIndent = -1;
     63     DEFINE_STATIC_LOCAL(String, prefix, ());
     64 
     65     if (indentLevel != previousIndent) {
     66         previousIndent = indentLevel;
     67         prefix.truncate(0);
     68         for (int i = 0; i < previousIndent; ++i)
     69             prefix += "    ";
     70     }
     71 
     72     return prefix;
     73 }
     74 
     75 static void pageCacheLog(const String& prefix, const String& message)
     76 {
     77     LOG(PageCache, "%s%s", prefix.utf8().data(), message.utf8().data());
     78 }
     79 
     80 #define PCLOG(...) pageCacheLog(pageCacheLogPrefix(indentLevel), makeString(__VA_ARGS__))
     81 
     82 static bool logCanCacheFrameDecision(Frame* frame, int indentLevel)
     83 {
     84     // Only bother logging for frames that have actually loaded and have content.
     85     if (frame->loader()->stateMachine()->creatingInitialEmptyDocument())
     86         return false;
     87     KURL currentURL = frame->loader()->documentLoader() ? frame->loader()->documentLoader()->url() : KURL();
     88     if (currentURL.isEmpty())
     89         return false;
     90 
     91     PCLOG("+---");
     92     KURL newURL = frame->loader()->provisionalDocumentLoader() ? frame->loader()->provisionalDocumentLoader()->url() : KURL();
     93     if (!newURL.isEmpty())
     94         PCLOG(" Determining if frame can be cached navigating from (", currentURL.string(), ") to (", newURL.string(), "):");
     95     else
     96         PCLOG(" Determining if subframe with URL (", currentURL.string(), ") can be cached:");
     97 
     98     bool cannotCache = false;
     99 
    100     do {
    101         if (!frame->loader()->documentLoader()) {
    102             PCLOG("   -There is no DocumentLoader object");
    103             cannotCache = true;
    104             break;
    105         }
    106         if (!frame->loader()->documentLoader()->mainDocumentError().isNull()) {
    107             PCLOG("   -Main document has an error");
    108             cannotCache = true;
    109         }
    110         if (frame->loader()->subframeLoader()->containsPlugins()) {
    111             PCLOG("   -Frame contains plugins");
    112             cannotCache = true;
    113         }
    114         if (frame->document()->url().protocolIs("https")) {
    115             PCLOG("   -Frame is HTTPS");
    116             cannotCache = true;
    117         }
    118         if (frame->domWindow() && frame->domWindow()->hasEventListeners(eventNames().unloadEvent)) {
    119             PCLOG("   -Frame has an unload event listener");
    120             cannotCache = true;
    121         }
    122 #if ENABLE(DATABASE)
    123         if (frame->document()->hasOpenDatabases()) {
    124             PCLOG("   -Frame has open database handles");
    125             cannotCache = true;
    126         }
    127 #endif
    128 #if ENABLE(SHARED_WORKERS)
    129         if (SharedWorkerRepository::hasSharedWorkers(frame->document())) {
    130             PCLOG("   -Frame has associated SharedWorkers");
    131             cannotCache = true;
    132         }
    133 #endif
    134         if (frame->document()->usingGeolocation()) {
    135             PCLOG("   -Frame uses Geolocation");
    136             cannotCache = true;
    137         }
    138         if (!frame->loader()->history()->currentItem()) {
    139             PCLOG("   -No current history item");
    140             cannotCache = true;
    141         }
    142         if (frame->loader()->quickRedirectComing()) {
    143             PCLOG("   -Quick redirect is coming");
    144             cannotCache = true;
    145         }
    146         if (frame->loader()->documentLoader()->isLoadingInAPISense()) {
    147             PCLOG("   -DocumentLoader is still loading in API sense");
    148             cannotCache = true;
    149         }
    150         if (frame->loader()->documentLoader()->isStopping()) {
    151             PCLOG("   -DocumentLoader is in the middle of stopping");
    152             cannotCache = true;
    153         }
    154         if (!frame->document()->canSuspendActiveDOMObjects()) {
    155             PCLOG("   -The document cannot suspect its active DOM Objects");
    156             cannotCache = true;
    157         }
    158 #if ENABLE(OFFLINE_WEB_APPLICATIONS)
    159         if (!frame->loader()->documentLoader()->applicationCacheHost()->canCacheInPageCache()) {
    160             PCLOG("   -The DocumentLoader uses an application cache");
    161             cannotCache = true;
    162         }
    163 #endif
    164         if (!frame->loader()->client()->canCachePage()) {
    165             PCLOG("   -The client says this frame cannot be cached");
    166             cannotCache = true;
    167         }
    168     } while (false);
    169 
    170     for (Frame* child = frame->tree()->firstChild(); child; child = child->tree()->nextSibling())
    171         if (!logCanCacheFrameDecision(child, indentLevel + 1))
    172             cannotCache = true;
    173 
    174     PCLOG(cannotCache ? " Frame CANNOT be cached" : " Frame CAN be cached");
    175     PCLOG("+---");
    176 
    177     return !cannotCache;
    178 }
    179 
    180 static void logCanCachePageDecision(Page* page)
    181 {
    182     // Only bother logging for main frames that have actually loaded and have content.
    183     if (page->mainFrame()->loader()->stateMachine()->creatingInitialEmptyDocument())
    184         return;
    185     KURL currentURL = page->mainFrame()->loader()->documentLoader() ? page->mainFrame()->loader()->documentLoader()->url() : KURL();
    186     if (currentURL.isEmpty())
    187         return;
    188 
    189     int indentLevel = 0;
    190     PCLOG("--------\n Determining if page can be cached:");
    191 
    192     bool cannotCache = !logCanCacheFrameDecision(page->mainFrame(), 1);
    193 
    194     FrameLoadType loadType = page->mainFrame()->loader()->loadType();
    195     if (!page->backForward()->isActive()) {
    196         PCLOG("   -The back/forward list is disabled or has 0 capacity");
    197         cannotCache = true;
    198     }
    199     if (!page->settings()->usesPageCache()) {
    200         PCLOG("   -Page settings says b/f cache disabled");
    201         cannotCache = true;
    202     }
    203 #if ENABLE(DEVICE_ORIENTATION)
    204     if (page->deviceMotionController() && page->deviceMotionController()->isActive()) {
    205         PCLOG("   -Page is using DeviceMotion");
    206         cannotCache = true;
    207     }
    208     if (page->deviceOrientationController() && page->deviceOrientationController()->isActive()) {
    209         PCLOG("   -Page is using DeviceOrientation");
    210         cannotCache = true;
    211     }
    212 #endif
    213     if (loadType == FrameLoadTypeReload) {
    214         PCLOG("   -Load type is: Reload");
    215         cannotCache = true;
    216     }
    217     if (loadType == FrameLoadTypeReloadFromOrigin) {
    218         PCLOG("   -Load type is: Reload from origin");
    219         cannotCache = true;
    220     }
    221     if (loadType == FrameLoadTypeSame) {
    222         PCLOG("   -Load type is: Same");
    223         cannotCache = true;
    224     }
    225 
    226     PCLOG(cannotCache ? " Page CANNOT be cached\n--------" : " Page CAN be cached\n--------");
    227 }
    228 
    229 #endif
    230 
    231 PageCache* pageCache()
    232 {
    233     static PageCache* staticPageCache = new PageCache;
    234     return staticPageCache;
    235 }
    236 
    237 PageCache::PageCache()
    238     : m_capacity(0)
    239     , m_size(0)
    240     , m_head(0)
    241     , m_tail(0)
    242     , m_autoreleaseTimer(this, &PageCache::releaseAutoreleasedPagesNowOrReschedule)
    243 {
    244 }
    245 
    246 bool PageCache::canCachePageContainingThisFrame(Frame* frame)
    247 {
    248     for (Frame* child = frame->tree()->firstChild(); child; child = child->tree()->nextSibling()) {
    249         if (!canCachePageContainingThisFrame(child))
    250             return false;
    251     }
    252 
    253     return frame->loader()->documentLoader()
    254         && frame->loader()->documentLoader()->mainDocumentError().isNull()
    255         // Do not cache error pages (these can be recognized as pages with substitute data or unreachable URLs).
    256         && !(frame->loader()->documentLoader()->substituteData().isValid() && !frame->loader()->documentLoader()->substituteData().failingURL().isEmpty())
    257         // FIXME: If we ever change this so that frames with plug-ins will be cached,
    258         // we need to make sure that we don't cache frames that have outstanding NPObjects
    259         // (objects created by the plug-in). Since there is no way to pause/resume a Netscape plug-in,
    260         // they would need to be destroyed and then recreated, and there is no way that we can recreate
    261         // the right NPObjects. See <rdar://problem/5197041> for more information.
    262         && !frame->loader()->subframeLoader()->containsPlugins()
    263         && !frame->document()->url().protocolIs("https")
    264         && (!frame->domWindow() || !frame->domWindow()->hasEventListeners(eventNames().unloadEvent))
    265 #if ENABLE(DATABASE)
    266         && !frame->document()->hasOpenDatabases()
    267 #endif
    268 #if ENABLE(SHARED_WORKERS)
    269         && !SharedWorkerRepository::hasSharedWorkers(frame->document())
    270 #endif
    271         && !frame->document()->usingGeolocation()
    272         && frame->loader()->history()->currentItem()
    273         && !frame->loader()->quickRedirectComing()
    274         && !frame->loader()->documentLoader()->isLoadingInAPISense()
    275         && !frame->loader()->documentLoader()->isStopping()
    276         && frame->document()->canSuspendActiveDOMObjects()
    277 #if ENABLE(OFFLINE_WEB_APPLICATIONS)
    278         // FIXME: We should investigating caching frames that have an associated
    279         // application cache. <rdar://problem/5917899> tracks that work.
    280         && frame->loader()->documentLoader()->applicationCacheHost()->canCacheInPageCache()
    281 #endif
    282 #if ENABLE(WML)
    283         && !frame->document()->containsWMLContent()
    284         && !frame->document()->isWMLDocument()
    285 #endif
    286         && frame->loader()->client()->canCachePage();
    287 }
    288 
    289 bool PageCache::canCache(Page* page)
    290 {
    291     if (!page)
    292         return false;
    293 
    294 #ifndef NDEBUG
    295     logCanCachePageDecision(page);
    296 #endif
    297 
    298     // Cache the page, if possible.
    299     // Don't write to the cache if in the middle of a redirect, since we will want to
    300     // store the final page we end up on.
    301     // No point writing to the cache on a reload or loadSame, since we will just write
    302     // over it again when we leave that page.
    303     // FIXME: <rdar://problem/4886592> - We should work out the complexities of caching pages with frames as they
    304     // are the most interesting pages on the web, and often those that would benefit the most from caching!
    305     FrameLoadType loadType = page->mainFrame()->loader()->loadType();
    306 
    307     return canCachePageContainingThisFrame(page->mainFrame())
    308         && page->backForward()->isActive()
    309         && page->settings()->usesPageCache()
    310 #if ENABLE(DEVICE_ORIENTATION)
    311         && !(page->deviceMotionController() && page->deviceMotionController()->isActive())
    312         && !(page->deviceOrientationController() && page->deviceOrientationController()->isActive())
    313 #endif
    314         && loadType != FrameLoadTypeReload
    315         && loadType != FrameLoadTypeReloadFromOrigin
    316         && loadType != FrameLoadTypeSame;
    317 }
    318 
    319 void PageCache::setCapacity(int capacity)
    320 {
    321     ASSERT(capacity >= 0);
    322     m_capacity = max(capacity, 0);
    323 
    324     prune();
    325 }
    326 
    327 int PageCache::frameCount() const
    328 {
    329     int frameCount = 0;
    330     for (HistoryItem* current = m_head; current; current = current->m_next) {
    331         ++frameCount;
    332         ASSERT(current->m_cachedPage);
    333         frameCount += current->m_cachedPage ? current->m_cachedPage->cachedMainFrame()->descendantFrameCount() : 0;
    334     }
    335 
    336     return frameCount;
    337 }
    338 
    339 int PageCache::autoreleasedPageCount() const
    340 {
    341     return m_autoreleaseSet.size();
    342 }
    343 
    344 void PageCache::markPagesForVistedLinkStyleRecalc()
    345 {
    346     for (HistoryItem* current = m_head; current; current = current->m_next)
    347         current->m_cachedPage->markForVistedLinkStyleRecalc();
    348 }
    349 
    350 void PageCache::add(PassRefPtr<HistoryItem> prpItem, Page* page)
    351 {
    352     ASSERT(prpItem);
    353     ASSERT(page);
    354     ASSERT(canCache(page));
    355 
    356     HistoryItem* item = prpItem.releaseRef(); // Balanced in remove().
    357 
    358     // Remove stale cache entry if necessary.
    359     if (item->m_cachedPage)
    360         remove(item);
    361 
    362     item->m_cachedPage = CachedPage::create(page);
    363     addToLRUList(item);
    364     ++m_size;
    365 
    366     prune();
    367 }
    368 
    369 CachedPage* PageCache::get(HistoryItem* item)
    370 {
    371     if (!item)
    372         return 0;
    373 
    374     if (CachedPage* cachedPage = item->m_cachedPage.get()) {
    375         // FIXME: 1800 should not be hardcoded, it should come from
    376         // WebKitBackForwardCacheExpirationIntervalKey in WebKit.
    377         // Or we should remove WebKitBackForwardCacheExpirationIntervalKey.
    378         if (currentTime() - cachedPage->timeStamp() <= 1800)
    379             return cachedPage;
    380 
    381         LOG(PageCache, "Not restoring page for %s from back/forward cache because cache entry has expired", item->url().string().ascii().data());
    382         pageCache()->remove(item);
    383     }
    384     return 0;
    385 }
    386 
    387 void PageCache::remove(HistoryItem* item)
    388 {
    389     // Safely ignore attempts to remove items not in the cache.
    390     if (!item || !item->m_cachedPage)
    391         return;
    392 
    393     autorelease(item->m_cachedPage.release());
    394     removeFromLRUList(item);
    395     --m_size;
    396 
    397     item->deref(); // Balanced in add().
    398 }
    399 
    400 void PageCache::prune()
    401 {
    402     while (m_size > m_capacity) {
    403         ASSERT(m_tail && m_tail->m_cachedPage);
    404         remove(m_tail);
    405     }
    406 }
    407 
    408 void PageCache::addToLRUList(HistoryItem* item)
    409 {
    410     item->m_next = m_head;
    411     item->m_prev = 0;
    412 
    413     if (m_head) {
    414         ASSERT(m_tail);
    415         m_head->m_prev = item;
    416     } else {
    417         ASSERT(!m_tail);
    418         m_tail = item;
    419     }
    420 
    421     m_head = item;
    422 }
    423 
    424 void PageCache::removeFromLRUList(HistoryItem* item)
    425 {
    426     if (!item->m_next) {
    427         ASSERT(item == m_tail);
    428         m_tail = item->m_prev;
    429     } else {
    430         ASSERT(item != m_tail);
    431         item->m_next->m_prev = item->m_prev;
    432     }
    433 
    434     if (!item->m_prev) {
    435         ASSERT(item == m_head);
    436         m_head = item->m_next;
    437     } else {
    438         ASSERT(item != m_head);
    439         item->m_prev->m_next = item->m_next;
    440     }
    441 }
    442 
    443 void PageCache::releaseAutoreleasedPagesNowOrReschedule(Timer<PageCache>* timer)
    444 {
    445     double loadDelta = currentTime() - FrameLoader::timeOfLastCompletedLoad();
    446     float userDelta = userIdleTime();
    447 
    448     // FIXME: <rdar://problem/5211190> This limit of 42 risks growing the page cache far beyond its nominal capacity.
    449     if ((userDelta < 0.5 || loadDelta < 1.25) && m_autoreleaseSet.size() < 42) {
    450         LOG(PageCache, "WebCorePageCache: Postponing releaseAutoreleasedPagesNowOrReschedule() - %f since last load, %f since last input, %i objects pending release", loadDelta, userDelta, m_autoreleaseSet.size());
    451         timer->startOneShot(autoreleaseInterval);
    452         return;
    453     }
    454 
    455     LOG(PageCache, "WebCorePageCache: Releasing page caches - %f seconds since last load, %f since last input, %i objects pending release", loadDelta, userDelta, m_autoreleaseSet.size());
    456     releaseAutoreleasedPagesNow();
    457 }
    458 
    459 void PageCache::releaseAutoreleasedPagesNow()
    460 {
    461     m_autoreleaseTimer.stop();
    462 
    463     // Postpone dead pruning until all our resources have gone dead.
    464     memoryCache()->setPruneEnabled(false);
    465 
    466     CachedPageSet tmp;
    467     tmp.swap(m_autoreleaseSet);
    468 
    469     CachedPageSet::iterator end = tmp.end();
    470     for (CachedPageSet::iterator it = tmp.begin(); it != end; ++it)
    471         (*it)->destroy();
    472 
    473     // Now do the prune.
    474     memoryCache()->setPruneEnabled(true);
    475     memoryCache()->prune();
    476 }
    477 
    478 void PageCache::autorelease(PassRefPtr<CachedPage> page)
    479 {
    480     ASSERT(page);
    481     ASSERT(!m_autoreleaseSet.contains(page.get()));
    482     m_autoreleaseSet.add(page);
    483     if (!m_autoreleaseTimer.isActive())
    484         m_autoreleaseTimer.startOneShot(autoreleaseInterval);
    485 }
    486 
    487 } // namespace WebCore
    488