blob: fa83219f34f1efc2575cd4a13736bc231982046d [file] [log] [blame]
/*
* Copyright (C) 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#if ENABLE(NETWORK_CACHE)
#include "NetworkCacheStatistics.h"
#include "Logging.h"
#include "NetworkCache.h"
#include "NetworkCacheFileSystemPosix.h"
#include "NetworkProcess.h"
#include <WebCore/DiagnosticLoggingKeys.h>
#include <WebCore/DiagnosticLoggingResultType.h>
#include <WebCore/ResourceRequest.h>
#include <WebCore/SQLiteDatabaseTracker.h>
#include <WebCore/SQLiteStatement.h>
#include <WebCore/SQLiteTransaction.h>
#include <wtf/RunLoop.h>
namespace WebKit {
namespace NetworkCache {
static const char* StatisticsDatabaseName = "WebKitCacheStatistics.db";
static const std::chrono::milliseconds mininumWriteInterval = std::chrono::milliseconds(10000);
static bool executeSQLCommand(WebCore::SQLiteDatabase& database, const String& sql)
{
ASSERT(!RunLoop::isMain());
ASSERT(WebCore::SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(database.isOpen());
bool result = database.executeCommand(sql);
if (!result)
LOG_ERROR("Network cache statistics: failed to execute statement \"%s\" error \"%s\"", sql.utf8().data(), database.lastErrorMsg());
return result;
}
static bool executeSQLStatement(WebCore::SQLiteStatement& statement)
{
ASSERT(!RunLoop::isMain());
ASSERT(WebCore::SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(statement.database().isOpen());
if (statement.step() != WebCore::SQLResultDone) {
LOG_ERROR("Network cache statistics: failed to execute statement \"%s\" error \"%s\"", statement.query().utf8().data(), statement.database().lastErrorMsg());
return false;
}
return true;
}
std::unique_ptr<Statistics> Statistics::open(const String& cachePath)
{
ASSERT(RunLoop::isMain());
String databasePath = WebCore::pathByAppendingComponent(cachePath, StatisticsDatabaseName);
return std::unique_ptr<Statistics>(new Statistics(databasePath));
}
Statistics::Statistics(const String& databasePath)
: m_serialBackgroundIOQueue(WorkQueue::create("com.apple.WebKit.Cache.Statistics.Background", WorkQueue::Type::Serial, WorkQueue::QOS::Background))
, m_writeTimer(*this, &Statistics::writeTimerFired)
{
initialize(databasePath);
}
void Statistics::initialize(const String& databasePath)
{
ASSERT(RunLoop::isMain());
auto startTime = std::chrono::system_clock::now();
StringCapture databasePathCapture(databasePath);
StringCapture networkCachePathCapture(singleton().storagePath());
serialBackgroundIOQueue().dispatch([this, databasePathCapture, networkCachePathCapture, startTime] {
WebCore::SQLiteTransactionInProgressAutoCounter transactionCounter;
String databasePath = databasePathCapture.string();
if (!WebCore::makeAllDirectories(WebCore::directoryName(databasePath)))
return;
LOG(NetworkCache, "(NetworkProcess) Opening network cache statistics database at %s...", databasePath.utf8().data());
m_database.open(databasePath);
m_database.disableThreadingChecks();
if (!m_database.isOpen()) {
LOG_ERROR("Network cache statistics: Failed to open / create the network cache statistics database");
return;
}
executeSQLCommand(m_database, ASCIILiteral("CREATE TABLE IF NOT EXISTS AlreadyRequested (hash TEXT PRIMARY KEY)"));
executeSQLCommand(m_database, ASCIILiteral("CREATE TABLE IF NOT EXISTS UncachedReason (hash TEXT PRIMARY KEY, reason INTEGER)"));
WebCore::SQLiteStatement statement(m_database, ASCIILiteral("SELECT count(*) FROM AlreadyRequested"));
if (statement.prepareAndStep() != WebCore::SQLResultRow) {
LOG_ERROR("Network cache statistics: Failed to count the number of rows in AlreadyRequested table");
return;
}
m_approximateEntryCount = statement.getColumnInt(0);
#if !LOG_DISABLED
auto elapsedMS = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime).count();
#endif
LOG(NetworkCache, "(NetworkProcess) Network cache statistics database load complete, entries=%lu time=%lldms", static_cast<size_t>(m_approximateEntryCount), elapsedMS);
if (!m_approximateEntryCount) {
bootstrapFromNetworkCache(networkCachePathCapture.string());
#if !LOG_DISABLED
elapsedMS = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime).count();
#endif
LOG(NetworkCache, "(NetworkProcess) Network cache statistics database bootstrapping complete, entries=%lu time=%lldms", static_cast<size_t>(m_approximateEntryCount), elapsedMS);
}
});
}
void Statistics::bootstrapFromNetworkCache(const String& networkCachePath)
{
ASSERT(!RunLoop::isMain());
LOG(NetworkCache, "(NetworkProcess) Bootstrapping the network cache statistics database from the network cache...");
Vector<StringCapture> hashes;
traverseCacheFiles(networkCachePath, [&hashes](const String& hash, const String&) {
hashes.append(hash);
});
WebCore::SQLiteTransactionInProgressAutoCounter transactionCounter;
WebCore::SQLiteTransaction writeTransaction(m_database);
writeTransaction.begin();
addHashesToDatabase(hashes);
writeTransaction.commit();
}
void Statistics::shrinkIfNeeded()
{
ASSERT(RunLoop::isMain());
const size_t maxEntries = 100000;
if (m_approximateEntryCount < maxEntries)
return;
LOG(NetworkCache, "(NetworkProcess) shrinking statistics cache m_approximateEntryCount=%lu, maxEntries=%lu", static_cast<size_t>(m_approximateEntryCount), maxEntries);
clear();
StringCapture networkCachePathCapture(singleton().storagePath());
serialBackgroundIOQueue().dispatch([this, networkCachePathCapture] {
bootstrapFromNetworkCache(networkCachePathCapture.string());
LOG(NetworkCache, "(NetworkProcess) statistics cache shrink completed m_approximateEntryCount=%lu", static_cast<size_t>(m_approximateEntryCount));
});
}
void Statistics::recordRetrievalRequest(uint64_t webPageID)
{
NetworkProcess::singleton().logDiagnosticMessage(webPageID, WebCore::DiagnosticLoggingKeys::networkCacheKey(), WebCore::DiagnosticLoggingKeys::retrievalRequestKey(), WebCore::ShouldSample::Yes);
}
void Statistics::recordNotCachingResponse(const Key& key, StoreDecision storeDecision)
{
ASSERT(storeDecision != StoreDecision::Yes);
m_storeDecisionsToAdd.set(key.hashAsString(), storeDecision);
if (!m_writeTimer.isActive())
m_writeTimer.startOneShot(mininumWriteInterval);
}
static String retrieveDecisionToDiagnosticKey(RetrieveDecision retrieveDecision)
{
switch (retrieveDecision) {
case RetrieveDecision::NoDueToHTTPMethod:
return WebCore::DiagnosticLoggingKeys::unsupportedHTTPMethodKey();
case RetrieveDecision::NoDueToConditionalRequest:
return WebCore::DiagnosticLoggingKeys::isConditionalRequestKey();
case RetrieveDecision::NoDueToReloadIgnoringCache:
return WebCore::DiagnosticLoggingKeys::isReloadIgnoringCacheDataKey();
case RetrieveDecision::Yes:
ASSERT_NOT_REACHED();
break;
}
return emptyString();
}
void Statistics::recordNotUsingCacheForRequest(uint64_t webPageID, const Key& key, const WebCore::ResourceRequest& request, RetrieveDecision retrieveDecision)
{
ASSERT(retrieveDecision != RetrieveDecision::Yes);
String hash = key.hashAsString();
WebCore::URL requestURL = request.url();
queryWasEverRequested(hash, NeedUncachedReason::No, [this, hash, requestURL, webPageID, retrieveDecision](bool wasEverRequested, const Optional<StoreDecision>&) {
if (wasEverRequested) {
String diagnosticKey = retrieveDecisionToDiagnosticKey(retrieveDecision);
LOG(NetworkCache, "(NetworkProcess) webPageID %llu: %s was previously requested but we are not using the cache, reason: %s", webPageID, requestURL.string().ascii().data(), diagnosticKey.utf8().data());
NetworkProcess::singleton().logDiagnosticMessageWithValue(webPageID, WebCore::DiagnosticLoggingKeys::networkCacheKey(), WebCore::DiagnosticLoggingKeys::unusedKey(), diagnosticKey, WebCore::ShouldSample::Yes);
} else {
NetworkProcess::singleton().logDiagnosticMessageWithValue(webPageID, WebCore::DiagnosticLoggingKeys::networkCacheKey(), WebCore::DiagnosticLoggingKeys::requestKey(), WebCore::DiagnosticLoggingKeys::neverSeenBeforeKey(), WebCore::ShouldSample::Yes);
markAsRequested(hash);
}
});
}
static String storeDecisionToDiagnosticKey(StoreDecision storeDecision)
{
switch (storeDecision) {
case StoreDecision::NoDueToProtocol:
return WebCore::DiagnosticLoggingKeys::notHTTPFamilyKey();
case StoreDecision::NoDueToHTTPMethod:
return WebCore::DiagnosticLoggingKeys::unsupportedHTTPMethodKey();
case StoreDecision::NoDueToAttachmentResponse:
return WebCore::DiagnosticLoggingKeys::isAttachmentKey();
case StoreDecision::NoDueToNoStoreResponse:
case StoreDecision::NoDueToNoStoreRequest:
return WebCore::DiagnosticLoggingKeys::cacheControlNoStoreKey();
case StoreDecision::NoDueToHTTPStatusCode:
return WebCore::DiagnosticLoggingKeys::uncacheableStatusCodeKey();
case StoreDecision::NoDueToUnlikelyToReuse:
return WebCore::DiagnosticLoggingKeys::unlikelyToReuseKey();
case StoreDecision::Yes:
// It was stored but could not be retrieved so it must have been pruned from the cache.
return WebCore::DiagnosticLoggingKeys::noLongerInCacheKey();
}
return String();
}
void Statistics::recordRetrievalFailure(uint64_t webPageID, const Key& key, const WebCore::ResourceRequest& request)
{
String hash = key.hashAsString();
WebCore::URL requestURL = request.url();
queryWasEverRequested(hash, NeedUncachedReason::Yes, [this, hash, requestURL, webPageID](bool wasPreviouslyRequested, const Optional<StoreDecision>& storeDecision) {
if (wasPreviouslyRequested) {
String diagnosticKey = storeDecisionToDiagnosticKey(storeDecision.value());
LOG(NetworkCache, "(NetworkProcess) webPageID %llu: %s was previously request but is not in the cache, reason: %s", webPageID, requestURL.string().ascii().data(), diagnosticKey.utf8().data());
NetworkProcess::singleton().logDiagnosticMessageWithValue(webPageID, WebCore::DiagnosticLoggingKeys::networkCacheKey(), WebCore::DiagnosticLoggingKeys::notInCacheKey(), diagnosticKey, WebCore::ShouldSample::Yes);
} else {
NetworkProcess::singleton().logDiagnosticMessageWithValue(webPageID, WebCore::DiagnosticLoggingKeys::networkCacheKey(), WebCore::DiagnosticLoggingKeys::requestKey(), WebCore::DiagnosticLoggingKeys::neverSeenBeforeKey(), WebCore::ShouldSample::Yes);
markAsRequested(hash);
}
});
}
static String cachedEntryReuseFailureToDiagnosticKey(UseDecision decision)
{
switch (decision) {
case UseDecision::NoDueToVaryingHeaderMismatch:
return WebCore::DiagnosticLoggingKeys::varyingHeaderMismatchKey();
case UseDecision::NoDueToMissingValidatorFields:
return WebCore::DiagnosticLoggingKeys::missingValidatorFieldsKey();
case UseDecision::NoDueToDecodeFailure:
return WebCore::DiagnosticLoggingKeys::otherKey();
case UseDecision::Use:
case UseDecision::Validate:
ASSERT_NOT_REACHED();
break;
}
return emptyString();
}
void Statistics::recordRetrievedCachedEntry(uint64_t webPageID, const Key& key, const WebCore::ResourceRequest& request, UseDecision decision)
{
WebCore::URL requestURL = request.url();
if (decision == UseDecision::Use || decision == UseDecision::Validate) {
LOG(NetworkCache, "(NetworkProcess) webPageID %llu: %s is in the cache and is used", webPageID, requestURL.string().ascii().data());
NetworkProcess::singleton().logDiagnosticMessageWithResult(webPageID, WebCore::DiagnosticLoggingKeys::networkCacheKey(), WebCore::DiagnosticLoggingKeys::retrievalKey(), WebCore::DiagnosticLoggingResultPass, WebCore::ShouldSample::Yes);
return;
}
String diagnosticKey = cachedEntryReuseFailureToDiagnosticKey(decision);
LOG(NetworkCache, "(NetworkProcess) webPageID %llu: %s is in the cache but wasn't used, reason: %s", webPageID, requestURL.string().ascii().data(), diagnosticKey.utf8().data());
NetworkProcess::singleton().logDiagnosticMessageWithValue(webPageID, WebCore::DiagnosticLoggingKeys::networkCacheKey(), WebCore::DiagnosticLoggingKeys::unusableCachedEntryKey(), diagnosticKey, WebCore::ShouldSample::Yes);
}
void Statistics::markAsRequested(const String& hash)
{
ASSERT(RunLoop::isMain());
m_hashesToAdd.add(hash);
if (!m_writeTimer.isActive())
m_writeTimer.startOneShot(mininumWriteInterval);
}
void Statistics::writeTimerFired()
{
ASSERT(RunLoop::isMain());
Vector<StringCapture> hashesToAdd;
copyToVector(m_hashesToAdd, hashesToAdd);
m_hashesToAdd.clear();
Vector<std::pair<StringCapture, StoreDecision>> storeDecisionsToAdd;
copyToVector(m_storeDecisionsToAdd, storeDecisionsToAdd);
m_storeDecisionsToAdd.clear();
shrinkIfNeeded();
serialBackgroundIOQueue().dispatch([this, hashesToAdd, storeDecisionsToAdd] {
if (!m_database.isOpen())
return;
WebCore::SQLiteTransactionInProgressAutoCounter transactionCounter;
WebCore::SQLiteTransaction writeTransaction(m_database);
writeTransaction.begin();
addHashesToDatabase(hashesToAdd);
addStoreDecisionsToDatabase(storeDecisionsToAdd);
writeTransaction.commit();
});
}
void Statistics::queryWasEverRequested(const String& hash, NeedUncachedReason needUncachedReason, const RequestedCompletionHandler& completionHandler)
{
ASSERT(RunLoop::isMain());
// Query pending writes first.
bool wasAlreadyRequested = m_hashesToAdd.contains(hash);
if (wasAlreadyRequested && needUncachedReason == NeedUncachedReason::No) {
completionHandler(true, Nullopt);
return;
}
if (needUncachedReason == NeedUncachedReason::Yes && m_storeDecisionsToAdd.contains(hash)) {
completionHandler(true, m_storeDecisionsToAdd.get(hash));
return;
}
// Query the database.
auto everRequestedQuery = std::make_unique<EverRequestedQuery>(EverRequestedQuery { hash, needUncachedReason == NeedUncachedReason::Yes, completionHandler });
auto& query = *everRequestedQuery;
m_activeQueries.add(WTF::move(everRequestedQuery));
serialBackgroundIOQueue().dispatch([this, wasAlreadyRequested, &query] () mutable {
WebCore::SQLiteTransactionInProgressAutoCounter transactionCounter;
Optional<StoreDecision> storeDecision;
if (m_database.isOpen()) {
if (!wasAlreadyRequested) {
WebCore::SQLiteStatement statement(m_database, ASCIILiteral("SELECT hash FROM AlreadyRequested WHERE hash=?"));
if (statement.prepare() == WebCore::SQLResultOk) {
statement.bindText(1, query.hash);
wasAlreadyRequested = (statement.step() == WebCore::SQLResultRow);
}
}
if (wasAlreadyRequested && query.needUncachedReason) {
WebCore::SQLiteStatement statement(m_database, ASCIILiteral("SELECT reason FROM UncachedReason WHERE hash=?"));
storeDecision = StoreDecision::Yes;
if (statement.prepare() == WebCore::SQLResultOk) {
statement.bindText(1, query.hash);
if (statement.step() == WebCore::SQLResultRow)
storeDecision = static_cast<StoreDecision>(statement.getColumnInt(0));
}
}
}
RunLoop::main().dispatch([this, &query, wasAlreadyRequested, storeDecision] {
query.completionHandler(wasAlreadyRequested, storeDecision);
m_activeQueries.remove(&query);
});
});
}
void Statistics::clear()
{
ASSERT(RunLoop::isMain());
serialBackgroundIOQueue().dispatch([this] {
if (m_database.isOpen()) {
WebCore::SQLiteTransactionInProgressAutoCounter transactionCounter;
WebCore::SQLiteTransaction deleteTransaction(m_database);
deleteTransaction.begin();
executeSQLCommand(m_database, ASCIILiteral("DELETE FROM AlreadyRequested"));
executeSQLCommand(m_database, ASCIILiteral("DELETE FROM UncachedReason"));
deleteTransaction.commit();
m_approximateEntryCount = 0;
}
});
}
void Statistics::addHashesToDatabase(const Vector<StringCapture>& hashes)
{
ASSERT(!RunLoop::isMain());
ASSERT(WebCore::SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(m_database.isOpen());
WebCore::SQLiteStatement statement(m_database, ASCIILiteral("INSERT OR IGNORE INTO AlreadyRequested (hash) VALUES (?)"));
if (statement.prepare() != WebCore::SQLResultOk)
return;
for (auto& hash : hashes) {
statement.bindText(1, hash.string());
if (executeSQLStatement(statement))
++m_approximateEntryCount;
statement.reset();
}
}
void Statistics::addStoreDecisionsToDatabase(const Vector<std::pair<StringCapture, StoreDecision>>& storeDecisions)
{
ASSERT(!RunLoop::isMain());
ASSERT(WebCore::SQLiteDatabaseTracker::hasTransactionInProgress());
ASSERT(m_database.isOpen());
WebCore::SQLiteStatement statement(m_database, ASCIILiteral("INSERT OR REPLACE INTO UncachedReason (hash, reason) VALUES (?, ?)"));
if (statement.prepare() != WebCore::SQLResultOk)
return;
for (auto& pair : storeDecisions) {
statement.bindText(1, pair.first.string());
statement.bindInt(2, static_cast<int>(pair.second));
executeSQLStatement(statement);
statement.reset();
}
}
}
}
#endif // ENABLE(NETWORK_CACHE)