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