Unreviewed, rolling out r239727.
[WebKit-https.git] / Source / WebCore / loader / ResourceLoadStatistics.cpp
index ac95013..c5ad746 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 Apple Inc.  All rights reserved.
+ * Copyright (C) 2016-2018 Apple Inc.  All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
 #include "ResourceLoadStatistics.h"
 
 #include "KeyedCoding.h"
+#include "PublicSuffix.h"
+#include <wtf/MainThread.h>
+#include <wtf/text/ASCIILiteral.h>
 #include <wtf/text/StringBuilder.h>
 #include <wtf/text/StringHash.h>
 
 namespace WebCore {
 
-static const unsigned minimumOriginsVisitedForPrevalenceClassification = 100;
+static Seconds timestampResolution { 5_s };
 
-// Sub frame thresholds
-static const unsigned subframeUnderTopFrameOriginsThresholdAbsolute = 3;
-
-// Subresource thresholds
-static const unsigned subresourceUnderTopFrameOriginsThresholdAbsolute = 5;
-static const unsigned subresourceHasBeenRedirectedFromToUniqueDomainsThresholdAbsolute = 3;
-static const unsigned redirectedToOtherPrevalentResourceOriginsThresholdAbsolute = 2;
+typedef WTF::HashMap<String, unsigned, StringHash, HashTraits<String>, HashTraits<unsigned>>::KeyValuePairType ResourceLoadStatisticsValue;
 
-bool ResourceLoadStatistics::checkAndSetAsPrevalentResourceIfNecessary(unsigned originsVisitedSoFar)
+static void encodeHashCountedSet(KeyedEncoder& encoder, const String& label, const HashCountedSet<String>& hashCountedSet)
 {
-    if (originsVisitedSoFar < minimumOriginsVisitedForPrevalenceClassification || isPrevalentResource)
-        return false;
-
-    if (hasPrevalentResourceCharacteristics()) {
-        isPrevalentResource = true;
-        return true;
-    }
+    if (hashCountedSet.isEmpty())
+        return;
 
-    return false;
+    encoder.encodeObjects(label, hashCountedSet.begin(), hashCountedSet.end(), [](KeyedEncoder& encoderInner, const ResourceLoadStatisticsValue& origin) {
+        encoderInner.encodeString("origin", origin.key);
+        encoderInner.encodeUInt32("count", origin.value);
+    });
 }
 
-bool ResourceLoadStatistics::hasPrevalentResourceCharacteristics() const
+static void encodeHashSet(KeyedEncoder& encoder, const String& label,  const String& key, const HashSet<String>& hashSet)
 {
-    return subframeUnderTopFrameOrigins.size() > subframeUnderTopFrameOriginsThresholdAbsolute
-        || subresourceUnderTopFrameOrigins.size() > subresourceUnderTopFrameOriginsThresholdAbsolute
-        || subresourceUniqueRedirectsTo.size() > subresourceHasBeenRedirectedFromToUniqueDomainsThresholdAbsolute
-        || redirectedToOtherPrevalentResourceOrigins.size() > redirectedToOtherPrevalentResourceOriginsThresholdAbsolute;
+    if (hashSet.isEmpty())
+        return;
+    
+    encoder.encodeObjects(label, hashSet.begin(), hashSet.end(), [&key](KeyedEncoder& encoderInner, const String& origin) {
+        encoderInner.encodeString(key, origin);
+    });
 }
 
-typedef WTF::HashMap<String, unsigned, StringHash, HashTraits<String>, HashTraits<unsigned>>::KeyValuePairType ResourceLoadStatisticsValue;
+static void encodeOriginHashSet(KeyedEncoder& encoder, const String& label, const HashSet<String>& hashSet)
+{
+    encodeHashSet(encoder, label, "origin", hashSet);
+}
 
-static void encodeHashCountedSet(KeyedEncoder& encoder, const String& label, const HashCountedSet<String>& hashCountedSet)
+template<typename T>
+static void encodeOptionSet(KeyedEncoder& encoder, const String& label, const OptionSet<T>& optionSet)
 {
-    if (hashCountedSet.isEmpty())
+    if (optionSet.isEmpty())
         return;
-
-    encoder.encodeObjects(label, hashCountedSet.begin(), hashCountedSet.end(), [](KeyedEncoder& encoderInner, const ResourceLoadStatisticsValue& origin) {
-        encoderInner.encodeString("origin", origin.key);
-        encoderInner.encodeUInt32("count", origin.value);
+    
+    uint64_t optionSetBitMask = optionSet.toRaw();
+    encoder.encodeUInt64(label, optionSetBitMask);
+}
+    
+static void encodeFontHashSet(KeyedEncoder& encoder, const String& label, const HashSet<String>& hashSet)
+{
+    encodeHashSet(encoder, label, "font", hashSet);
+}
+    
+static void encodeCanvasActivityRecord(KeyedEncoder& encoder, const String& label, const CanvasActivityRecord& canvasActivityRecord)
+{
+    encoder.encodeObject(label, canvasActivityRecord, [] (KeyedEncoder& encoderInner, const CanvasActivityRecord& canvasActivityRecord) {
+        encoderInner.encodeBool("wasDataRead", canvasActivityRecord.wasDataRead);
+        encoderInner.encodeObjects("textWritten", canvasActivityRecord.textWritten.begin(), canvasActivityRecord.textWritten.end(), [] (KeyedEncoder& encoderInner2, const String& text) {
+            encoderInner2.encodeString("text", text);
+        });
     });
 }
 
-void ResourceLoadStatistics::encode(KeyedEncoder& encoder, const String& origin) const
+void ResourceLoadStatistics::encode(KeyedEncoder& encoder) const
 {
-    encoder.encodeString("PrevalentResourceOrigin", origin);
+    encoder.encodeString("PrevalentResourceOrigin", highLevelDomain);
+    
+    encoder.encodeDouble("lastSeen", lastSeen.secondsSinceEpoch().value());
     
     // User interaction
     encoder.encodeBool("hadUserInteraction", hadUserInteraction);
-    
+    encoder.encodeDouble("mostRecentUserInteraction", mostRecentUserInteractionTime.secondsSinceEpoch().value());
+    encoder.encodeBool("grandfathered", grandfathered);
+
+    // Storage access
+    encodeOriginHashSet(encoder, "storageAccessUnderTopFrameOrigins", storageAccessUnderTopFrameOrigins);
+
     // Top frame stats
-    encoder.encodeBool("topFrameHasBeenNavigatedToBefore", topFrameHasBeenNavigatedToBefore);
-    encoder.encodeUInt32("topFrameHasBeenRedirectedTo", topFrameHasBeenRedirectedTo);
-    encoder.encodeUInt32("topFrameHasBeenRedirectedFrom", topFrameHasBeenRedirectedFrom);
-    encoder.encodeUInt32("topFrameInitialLoadCount", topFrameInitialLoadCount);
-    encoder.encodeUInt32("topFrameHasBeenNavigatedTo", topFrameHasBeenNavigatedTo);
-    encoder.encodeUInt32("topFrameHasBeenNavigatedFrom", topFrameHasBeenNavigatedFrom);
-    
+    encodeHashCountedSet(encoder, "topFrameUniqueRedirectsTo", topFrameUniqueRedirectsTo);
+    encodeHashCountedSet(encoder, "topFrameUniqueRedirectsFrom", topFrameUniqueRedirectsFrom);
+
     // Subframe stats
-    encoder.encodeBool("subframeHasBeenLoadedBefore", subframeHasBeenLoadedBefore);
-    encoder.encodeUInt32("subframeHasBeenRedirectedTo", subframeHasBeenRedirectedTo);
-    encoder.encodeUInt32("subframeHasBeenRedirectedFrom", subframeHasBeenRedirectedFrom);
-    encoder.encodeUInt32("subframeSubResourceCount", subframeSubResourceCount);
     encodeHashCountedSet(encoder, "subframeUnderTopFrameOrigins", subframeUnderTopFrameOrigins);
-    encodeHashCountedSet(encoder, "subframeUniqueRedirectsTo", subframeUniqueRedirectsTo);
-    encoder.encodeUInt32("subframeHasBeenNavigatedTo", subframeHasBeenNavigatedTo);
-    encoder.encodeUInt32("subframeHasBeenNavigatedFrom", subframeHasBeenNavigatedFrom);
     
     // Subresource stats
-    encoder.encodeUInt32("subresourceHasBeenRedirectedFrom", subresourceHasBeenRedirectedFrom);
-    encoder.encodeUInt32("subresourceHasBeenRedirectedTo", subresourceHasBeenRedirectedTo);
-    encoder.encodeUInt32("subresourceHasBeenSubresourceCount", subresourceHasBeenSubresourceCount);
-    encoder.encodeDouble("subresourceHasBeenSubresourceCountDividedByTotalNumberOfOriginsVisited", subresourceHasBeenSubresourceCountDividedByTotalNumberOfOriginsVisited);
     encodeHashCountedSet(encoder, "subresourceUnderTopFrameOrigins", subresourceUnderTopFrameOrigins);
     encodeHashCountedSet(encoder, "subresourceUniqueRedirectsTo", subresourceUniqueRedirectsTo);
-    
+    encodeHashCountedSet(encoder, "subresourceUniqueRedirectsFrom", subresourceUniqueRedirectsFrom);
+
     // Prevalent Resource
-    encodeHashCountedSet(encoder, "redirectedToOtherPrevalentResourceOrigins", redirectedToOtherPrevalentResourceOrigins);
     encoder.encodeBool("isPrevalentResource", isPrevalentResource);
+    encoder.encodeBool("isVeryPrevalentResource", isVeryPrevalentResource);
+    encoder.encodeUInt32("dataRecordsRemoved", dataRecordsRemoved);
+
+    encoder.encodeUInt32("timesAccessedAsFirstPartyDueToUserInteraction", timesAccessedAsFirstPartyDueToUserInteraction);
+    encoder.encodeUInt32("timesAccessedAsFirstPartyDueToStorageAccessAPI", timesAccessedAsFirstPartyDueToStorageAccessAPI);
+    
+    encodeFontHashSet(encoder, "fontsFailedToLoad", fontsFailedToLoad);
+    encodeFontHashSet(encoder, "fontsSuccessfullyLoaded", fontsSuccessfullyLoaded);
+    encodeHashCountedSet(encoder, "topFrameRegistrableDomainsWhichAccessedWebAPIs", topFrameRegistrableDomainsWhichAccessedWebAPIs);
+    encodeCanvasActivityRecord(encoder, "canvasActivityRecord", canvasActivityRecord);
+    encodeOptionSet(encoder, "navigatorFunctionsAccessedBitMask", navigatorFunctionsAccessed);
+    encodeOptionSet(encoder, "screenFunctionsAccessedBitMask", screenFunctionsAccessed);
 }
 
 static void decodeHashCountedSet(KeyedDecoder& decoder, const String& label, HashCountedSet<String>& hashCountedSet)
@@ -124,97 +142,141 @@ static void decodeHashCountedSet(KeyedDecoder& decoder, const String& label, Has
         unsigned count;
         if (!decoderInner.decodeUInt32("count", count))
             return false;
+
+        hashCountedSet.add(origin, count);
+        return true;
+    });
+}
+
+static void decodeHashSet(KeyedDecoder& decoder, const String& label, const String& key, HashSet<String>& hashSet)
+{
+    Vector<String> ignore;
+    decoder.decodeObjects(label, ignore, [&hashSet, &key](KeyedDecoder& decoderInner, String& origin) {
+        if (!decoderInner.decodeString(key, origin))
+            return false;
         
-        // FIXME: Create a HashCountedSet method to do this efficiently
-        for (unsigned i = 0; i < count; ++i)
-            hashCountedSet.add(origin);
+        hashSet.add(origin);
         return true;
     });
 }
 
-bool ResourceLoadStatistics::decode(KeyedDecoder& decoder, const String& origin)
+static void decodeOriginHashSet(KeyedDecoder& decoder, const String& label, HashSet<String>& hashSet)
 {
-    String storedOrigin;
-    if (!decoder.decodeString("PrevalentResourceOrigin", storedOrigin))
-        return false;
+    decodeHashSet(decoder, label, "origin", hashSet);
+}
+
+template<typename T>
+static void decodeOptionSet(KeyedDecoder& decoder, const String& label, OptionSet<T>& optionSet)
+{
+    uint64_t optionSetBitMask = 0;
+    decoder.decodeUInt64(label, optionSetBitMask);
+    optionSet = OptionSet<T>::fromRaw(optionSetBitMask);
+}
     
-    ASSERT_UNUSED(origin, storedOrigin == origin);
+static void decodeFontHashSet(KeyedDecoder& decoder, const String& label, HashSet<String>& hashSet)
+{
+    decodeHashSet(decoder, label, "font", hashSet);
+}
+    
+static void decodeCanvasActivityRecord(KeyedDecoder& decoder, const String& label, CanvasActivityRecord& canvasActivityRecord)
+{
+    decoder.decodeObject(label, canvasActivityRecord, [] (KeyedDecoder& decoderInner, CanvasActivityRecord& canvasActivityRecord) {
+        if (!decoderInner.decodeBool("wasDataRead", canvasActivityRecord.wasDataRead))
+            return false;
+        Vector<String> ignore;
+        decoderInner.decodeObjects("textWritten", ignore, [&canvasActivityRecord] (KeyedDecoder& decoderInner2, String& text) {
+            if (!decoderInner2.decodeString("text", text))
+                return false;
+            canvasActivityRecord.textWritten.add(text);
+            return true;
+        });
+        return true;
+    });
+}
+
+bool ResourceLoadStatistics::decode(KeyedDecoder& decoder, unsigned modelVersion)
+{
+    if (!decoder.decodeString("PrevalentResourceOrigin", highLevelDomain))
+        return false;
     
     // User interaction
     if (!decoder.decodeBool("hadUserInteraction", hadUserInteraction))
         return false;
-    
+
+    // Storage access
+    decodeOriginHashSet(decoder, "storageAccessUnderTopFrameOrigins", storageAccessUnderTopFrameOrigins);
+
     // Top frame stats
-    if (!decoder.decodeBool("topFrameHasBeenNavigatedToBefore", topFrameHasBeenNavigatedToBefore))
-        return false;
-    
-    if (!decoder.decodeUInt32("topFrameHasBeenRedirectedTo", topFrameHasBeenRedirectedTo))
-        return false;
-    
-    if (!decoder.decodeUInt32("topFrameHasBeenRedirectedFrom", topFrameHasBeenRedirectedFrom))
-        return false;
-    
-    if (!decoder.decodeUInt32("topFrameInitialLoadCount", topFrameInitialLoadCount))
-        return false;
-    
-    if (!decoder.decodeUInt32("topFrameHasBeenNavigatedTo", topFrameHasBeenNavigatedTo))
-        return false;
-    
-    if (!decoder.decodeUInt32("topFrameHasBeenNavigatedFrom", topFrameHasBeenNavigatedFrom))
-        return false;
-    
+    if (modelVersion >= 11) {
+        decodeHashCountedSet(decoder, "topFrameUniqueRedirectsTo", topFrameUniqueRedirectsTo);
+        decodeHashCountedSet(decoder, "topFrameUniqueRedirectsFrom", topFrameUniqueRedirectsFrom);
+    }
+
     // Subframe stats
-    if (!decoder.decodeBool("subframeHasBeenLoadedBefore", subframeHasBeenLoadedBefore))
-        return false;
-    
-    if (!decoder.decodeUInt32("subframeHasBeenRedirectedTo", subframeHasBeenRedirectedTo))
-        return false;
-    
-    if (!decoder.decodeUInt32("subframeHasBeenRedirectedFrom", subframeHasBeenRedirectedFrom))
-        return false;
-    
-    if (!decoder.decodeUInt32("subframeSubResourceCount", subframeSubResourceCount))
-        return false;
+    if (modelVersion >= 14)
+        decodeHashCountedSet(decoder, "subframeUnderTopFrameOrigins", subframeUnderTopFrameOrigins);
 
-    decodeHashCountedSet(decoder, "subframeUnderTopFrameOrigins", subframeUnderTopFrameOrigins);
-    decodeHashCountedSet(decoder, "subframeUniqueRedirectsTo", subframeUniqueRedirectsTo);
-    
-    if (!decoder.decodeUInt32("subframeHasBeenNavigatedTo", subframeHasBeenNavigatedTo))
-        return false;
-    
-    if (!decoder.decodeUInt32("subframeHasBeenNavigatedFrom", subframeHasBeenNavigatedFrom))
-        return false;
-    
     // Subresource stats
-    if (!decoder.decodeUInt32("subresourceHasBeenRedirectedFrom", subresourceHasBeenRedirectedFrom))
+    decodeHashCountedSet(decoder, "subresourceUnderTopFrameOrigins", subresourceUnderTopFrameOrigins);
+    decodeHashCountedSet(decoder, "subresourceUniqueRedirectsTo", subresourceUniqueRedirectsTo);
+    if (modelVersion >= 11)
+        decodeHashCountedSet(decoder, "subresourceUniqueRedirectsFrom", subresourceUniqueRedirectsFrom);
+
+    // Prevalent Resource
+    if (!decoder.decodeBool("isPrevalentResource", isPrevalentResource))
         return false;
-    
-    if (!decoder.decodeUInt32("subresourceHasBeenRedirectedTo", subresourceHasBeenRedirectedTo))
+
+    if (modelVersion >= 12) {
+        if (!decoder.decodeBool("isVeryPrevalentResource", isVeryPrevalentResource))
+            return false;
+    }
+
+    // Trigger re-classification based on model 14.
+    if (modelVersion < 14) {
+        isPrevalentResource = false;
+        isVeryPrevalentResource = false;
+    }
+
+    if (!decoder.decodeUInt32("dataRecordsRemoved", dataRecordsRemoved))
         return false;
-    
-    if (!decoder.decodeUInt32("subresourceHasBeenSubresourceCount", subresourceHasBeenSubresourceCount))
+
+    double mostRecentUserInteractionTimeAsDouble;
+    if (!decoder.decodeDouble("mostRecentUserInteraction", mostRecentUserInteractionTimeAsDouble))
         return false;
-    
-    if (!decoder.decodeDouble("subresourceHasBeenSubresourceCountDividedByTotalNumberOfOriginsVisited", subresourceHasBeenSubresourceCountDividedByTotalNumberOfOriginsVisited))
+    mostRecentUserInteractionTime = WallTime::fromRawSeconds(mostRecentUserInteractionTimeAsDouble);
+
+    if (!decoder.decodeBool("grandfathered", grandfathered))
         return false;
 
-    decodeHashCountedSet(decoder, "subresourceUnderTopFrameOrigins", subresourceUnderTopFrameOrigins);
-    decodeHashCountedSet(decoder, "subresourceUniqueRedirectsTo", subresourceUniqueRedirectsTo);
-    
-    // Prevalent Resource
-    decodeHashCountedSet(decoder, "redirectedToOtherPrevalentResourceOrigins", redirectedToOtherPrevalentResourceOrigins);
-    
-    if (!decoder.decodeBool("isPrevalentResource", isPrevalentResource))
+    double lastSeenTimeAsDouble;
+    if (!decoder.decodeDouble("lastSeen", lastSeenTimeAsDouble))
         return false;
+    lastSeen = WallTime::fromRawSeconds(lastSeenTimeAsDouble);
+
+    if (modelVersion >= 11) {
+        if (!decoder.decodeUInt32("timesAccessedAsFirstPartyDueToUserInteraction", timesAccessedAsFirstPartyDueToUserInteraction))
+            timesAccessedAsFirstPartyDueToUserInteraction = 0;
+        if (!decoder.decodeUInt32("timesAccessedAsFirstPartyDueToStorageAccessAPI", timesAccessedAsFirstPartyDueToStorageAccessAPI))
+            timesAccessedAsFirstPartyDueToStorageAccessAPI = 0;
+    }
+    
+    if (modelVersion >= 13) {
+        decodeFontHashSet(decoder, "fontsFailedToLoad", fontsFailedToLoad);
+        decodeFontHashSet(decoder, "fontsSuccessfullyLoaded", fontsSuccessfullyLoaded);
+        decodeHashCountedSet(decoder, "topFrameRegistrableDomainsWhichAccessedWebAPIs", topFrameRegistrableDomainsWhichAccessedWebAPIs);
+        decodeCanvasActivityRecord(decoder, "canvasActivityRecord", canvasActivityRecord);
+        decodeOptionSet(decoder, "navigatorFunctionsAccessedBitMask", navigatorFunctionsAccessed);
+        decodeOptionSet(decoder, "screenFunctionsAccessedBitMask", screenFunctionsAccessed);
+    }
     
     return true;
 }
 
 static void appendBoolean(StringBuilder& builder, const String& label, bool flag)
 {
-    builder.append("    ");
+    builder.appendLiteral("    ");
     builder.append(label);
-    builder.append(": ");
+    builder.appendLiteral(": ");
     builder.append(flag ? "Yes" : "No");
 }
 
@@ -223,90 +285,246 @@ static void appendHashCountedSet(StringBuilder& builder, const String& label, co
     if (hashCountedSet.isEmpty())
         return;
 
-    builder.append("    ");
+    builder.appendLiteral("    ");
     builder.append(label);
-    builder.append(":\n");
+    builder.appendLiteral(":\n");
 
     for (auto& entry : hashCountedSet) {
-        builder.append("        ");
+        builder.appendLiteral("        ");
         builder.append(entry.key);
-        builder.append(": ");
+        builder.appendLiteral(": ");
         builder.appendNumber(entry.value);
         builder.append('\n');
     }
+}
+
+static void appendHashSet(StringBuilder& builder, const String& label, const HashSet<String>& hashSet)
+{
+    if (hashSet.isEmpty())
+        return;
+    
+    builder.appendLiteral("    ");
+    builder.append(label);
+    builder.appendLiteral(":\n");
+    
+    for (auto& entry : hashSet) {
+        builder.appendLiteral("        ");
+        builder.append(entry);
+        builder.append('\n');
+    }
+}
+
+static ASCIILiteral navigatorAPIEnumToString(ResourceLoadStatistics::NavigatorAPI navigatorEnum)
+{
+    switch (navigatorEnum) {
+    case ResourceLoadStatistics::NavigatorAPI::JavaEnabled:
+        return "javaEnabled"_s;
+    case ResourceLoadStatistics::NavigatorAPI::MimeTypes:
+        return "mimeTypes"_s;
+    case ResourceLoadStatistics::NavigatorAPI::CookieEnabled:
+        return "cookieEnabled"_s;
+    case ResourceLoadStatistics::NavigatorAPI::Plugins:
+        return "plugins"_s;
+    case ResourceLoadStatistics::NavigatorAPI::UserAgent:
+        return "userAgent"_s;
+    case ResourceLoadStatistics::NavigatorAPI::AppVersion:
+        return "appVersion"_s;
+    }
+    ASSERT_NOT_REACHED();
+    return "Invalid navigator API"_s;
+}
+
+static ASCIILiteral screenAPIEnumToString(ResourceLoadStatistics::ScreenAPI screenEnum)
+{
+    switch (screenEnum) {
+    case ResourceLoadStatistics::ScreenAPI::Height:
+        return "height"_s;
+    case ResourceLoadStatistics::ScreenAPI::Width:
+        return "width"_s;
+    case ResourceLoadStatistics::ScreenAPI::ColorDepth:
+        return "colorDepth"_s;
+    case ResourceLoadStatistics::ScreenAPI::PixelDepth:
+        return "pixelDepth"_s;
+    case ResourceLoadStatistics::ScreenAPI::AvailLeft:
+        return "availLeft"_s;
+    case ResourceLoadStatistics::ScreenAPI::AvailTop:
+        return "availTop"_s;
+    case ResourceLoadStatistics::ScreenAPI::AvailHeight:
+        return "availHeight"_s;
+    case ResourceLoadStatistics::ScreenAPI::AvailWidth:
+        return "availWidth"_s;
+    }
+    ASSERT_NOT_REACHED();
+    return "Invalid screen API"_s;
+}
+    
+static void appendNavigatorAPIOptionSet(StringBuilder& builder, const OptionSet<ResourceLoadStatistics::NavigatorAPI>& optionSet)
+{
+    if (optionSet.isEmpty())
+        return;
+    builder.appendLiteral("    navigatorFunctionsAccessed:\n");
+    for (auto navigatorAPI : optionSet) {
+        builder.appendLiteral("        ");
+        builder.append(navigatorAPIEnumToString(navigatorAPI).characters());
+        builder.append('\n');
+    }
+}
     
+static void appendScreenAPIOptionSet(StringBuilder& builder, const OptionSet<ResourceLoadStatistics::ScreenAPI>& optionSet)
+{
+    if (optionSet.isEmpty())
+        return;
+    builder.appendLiteral("    screenFunctionsAccessed:\n");
+    for (auto screenAPI : optionSet) {
+        builder.appendLiteral("        ");
+        builder.append(screenAPIEnumToString(screenAPI).characters());
+        builder.append('\n');
+    }
 }
 
 String ResourceLoadStatistics::toString() const
 {
     StringBuilder builder;
+    builder.appendLiteral("High level domain: ");
+    builder.append(highLevelDomain);
+    builder.append('\n');
+    builder.appendLiteral("    lastSeen: ");
+    builder.appendNumber(lastSeen.secondsSinceEpoch().value());
+    builder.append('\n');
     
     // User interaction
     appendBoolean(builder, "hadUserInteraction", hadUserInteraction);
     builder.append('\n');
-    
-    // Top frame stats
-    appendBoolean(builder, "topFrameHasBeenNavigatedToBefore", topFrameHasBeenNavigatedToBefore);
-    builder.append('\n');
-    builder.append("    topFrameHasBeenRedirectedTo: ");
-    builder.appendNumber(topFrameHasBeenRedirectedTo);
-    builder.append('\n');
-    builder.append("    topFrameHasBeenRedirectedFrom: ");
-    builder.appendNumber(topFrameHasBeenRedirectedFrom);
-    builder.append('\n');
-    builder.append("    topFrameInitialLoadCount: ");
-    builder.appendNumber(topFrameInitialLoadCount);
-    builder.append('\n');
-    builder.append("    topFrameHasBeenNavigatedTo: ");
-    builder.appendNumber(topFrameHasBeenNavigatedTo);
+    builder.appendLiteral("    mostRecentUserInteraction: ");
+    builder.appendNumber(mostRecentUserInteractionTime.secondsSinceEpoch().value());
     builder.append('\n');
-    builder.append("    topFrameHasBeenNavigatedFrom: ");
-    builder.appendNumber(topFrameHasBeenNavigatedFrom);
+    appendBoolean(builder, "grandfathered", grandfathered);
     builder.append('\n');
-    
+
+    // Storage access
+    appendHashSet(builder, "storageAccessUnderTopFrameOrigins", storageAccessUnderTopFrameOrigins);
+
+    // Top frame stats
+    appendHashCountedSet(builder, "topFrameUniqueRedirectsTo", topFrameUniqueRedirectsTo);
+    appendHashCountedSet(builder, "topFrameUniqueRedirectsFrom", topFrameUniqueRedirectsFrom);
+
     // Subframe stats
-    appendBoolean(builder, "subframeHasBeenLoadedBefore", subframeHasBeenLoadedBefore);
-    builder.append('\n');
-    builder.append("    subframeHasBeenRedirectedTo: ");
-    builder.appendNumber(subframeHasBeenRedirectedTo);
-    builder.append('\n');
-    builder.append("    subframeHasBeenRedirectedFrom: ");
-    builder.appendNumber(subframeHasBeenRedirectedFrom);
-    builder.append('\n');
-    builder.append("    subframeSubResourceCount: ");
-    builder.appendNumber(subframeSubResourceCount);
-    builder.append('\n');
     appendHashCountedSet(builder, "subframeUnderTopFrameOrigins", subframeUnderTopFrameOrigins);
-    appendHashCountedSet(builder, "subframeUniqueRedirectsTo", subframeUniqueRedirectsTo);
-    builder.append("    subframeHasBeenNavigatedTo: ");
-    builder.appendNumber(subframeHasBeenNavigatedTo);
-    builder.append('\n');
-    builder.append("    subframeHasBeenNavigatedFrom: ");
-    builder.appendNumber(subframeHasBeenNavigatedFrom);
-    builder.append('\n');
     
     // Subresource stats
-    builder.append("    subresourceHasBeenRedirectedFrom: ");
-    builder.appendNumber(subresourceHasBeenRedirectedFrom);
-    builder.append('\n');
-    builder.append("    subresourceHasBeenRedirectedTo: ");
-    builder.appendNumber(subresourceHasBeenRedirectedTo);
-    builder.append('\n');
-    builder.append("    subresourceHasBeenSubresourceCount: ");
-    builder.appendNumber(subresourceHasBeenSubresourceCount);
-    builder.append('\n');
-    builder.append("    subresourceHasBeenSubresourceCountDividedByTotalNumberOfOriginsVisited: ");
-    builder.appendNumber(subresourceHasBeenSubresourceCountDividedByTotalNumberOfOriginsVisited);
-    builder.append('\n');
     appendHashCountedSet(builder, "subresourceUnderTopFrameOrigins", subresourceUnderTopFrameOrigins);
     appendHashCountedSet(builder, "subresourceUniqueRedirectsTo", subresourceUniqueRedirectsTo);
-    
+    appendHashCountedSet(builder, "subresourceUniqueRedirectsFrom", subresourceUniqueRedirectsFrom);
+
     // Prevalent Resource
-    appendHashCountedSet(builder, "redirectedToOtherPrevalentResourceOrigins", redirectedToOtherPrevalentResourceOrigins);
     appendBoolean(builder, "isPrevalentResource", isPrevalentResource);
     builder.append('\n');
+    appendBoolean(builder, "isVeryPrevalentResource", isVeryPrevalentResource);
+    builder.append('\n');
+    builder.appendLiteral("    dataRecordsRemoved: ");
+    builder.appendNumber(dataRecordsRemoved);
+    builder.append('\n');
+
+    appendHashSet(builder, "fontsFailedToLoad", fontsFailedToLoad);
+    appendHashSet(builder, "fontsSuccessfullyLoaded", fontsSuccessfullyLoaded);
+    appendHashCountedSet(builder, "topFrameRegistrableDomainsWhichAccessedWebAPIs", topFrameRegistrableDomainsWhichAccessedWebAPIs);
+    appendNavigatorAPIOptionSet(builder, navigatorFunctionsAccessed);
+    appendScreenAPIOptionSet(builder, screenFunctionsAccessed);
+    appendHashSet(builder, "canvasTextWritten", canvasActivityRecord.textWritten);
+    appendBoolean(builder, "canvasReadData", canvasActivityRecord.wasDataRead);
+    builder.append('\n');
+    builder.append('\n');
 
     return builder.toString();
 }
 
+template <typename T>
+static void mergeHashCountedSet(HashCountedSet<T>& to, const HashCountedSet<T>& from)
+{
+    for (auto& entry : from)
+        to.add(entry.key, entry.value);
+}
+
+template <typename T>
+static void mergeHashSet(HashSet<T>& to, const HashSet<T>& from)
+{
+    for (auto& entry : from)
+        to.add(entry);
+}
+
+void ResourceLoadStatistics::merge(const ResourceLoadStatistics& other)
+{
+    ASSERT(other.highLevelDomain == highLevelDomain);
+
+    if (lastSeen < other.lastSeen)
+        lastSeen = other.lastSeen;
+    
+    if (!other.hadUserInteraction) {
+        // If user interaction has been reset do so here too.
+        // Else, do nothing.
+        if (!other.mostRecentUserInteractionTime) {
+            hadUserInteraction = false;
+            mostRecentUserInteractionTime = { };
+        }
+    } else {
+        hadUserInteraction = true;
+        if (mostRecentUserInteractionTime < other.mostRecentUserInteractionTime)
+            mostRecentUserInteractionTime = other.mostRecentUserInteractionTime;
+    }
+    grandfathered |= other.grandfathered;
+
+    // Storage access
+    mergeHashSet(storageAccessUnderTopFrameOrigins, other.storageAccessUnderTopFrameOrigins);
+
+    // Top frame stats
+    mergeHashCountedSet(topFrameUniqueRedirectsTo, other.topFrameUniqueRedirectsTo);
+    mergeHashCountedSet(topFrameUniqueRedirectsFrom, other.topFrameUniqueRedirectsFrom);
+
+    // Subframe stats
+    mergeHashCountedSet(subframeUnderTopFrameOrigins, other.subframeUnderTopFrameOrigins);
+    
+    // Subresource stats
+    mergeHashCountedSet(subresourceUnderTopFrameOrigins, other.subresourceUnderTopFrameOrigins);
+    mergeHashCountedSet(subresourceUniqueRedirectsTo, other.subresourceUniqueRedirectsTo);
+    mergeHashCountedSet(subresourceUniqueRedirectsFrom, other.subresourceUniqueRedirectsFrom);
+
+    // Prevalent resource stats
+    isPrevalentResource |= other.isPrevalentResource;
+    isVeryPrevalentResource |= other.isVeryPrevalentResource;
+    dataRecordsRemoved = std::max(dataRecordsRemoved, other.dataRecordsRemoved);
+    
+    mergeHashSet(fontsFailedToLoad, other.fontsFailedToLoad);
+    mergeHashSet(fontsSuccessfullyLoaded, other.fontsSuccessfullyLoaded);
+    mergeHashCountedSet(topFrameRegistrableDomainsWhichAccessedWebAPIs, other.topFrameRegistrableDomainsWhichAccessedWebAPIs);
+    canvasActivityRecord.mergeWith(other.canvasActivityRecord);
+    navigatorFunctionsAccessed.add(other.navigatorFunctionsAccessed);
+    screenFunctionsAccessed.add(other.screenFunctionsAccessed);
+}
+
+String ResourceLoadStatistics::primaryDomain(const URL& url)
+{
+    return primaryDomain(url.host());
+}
+
+String ResourceLoadStatistics::primaryDomain(StringView host)
+{
+    if (host.isNull() || host.isEmpty())
+        return "nullOrigin"_s;
+
+    String hostString = host.toString();
+#if ENABLE(PUBLIC_SUFFIX_LIST)
+    String primaryDomain = topPrivatelyControlledDomain(hostString);
+    // We will have an empty string here if there is no TLD. Use the host as a fallback.
+    if (!primaryDomain.isEmpty())
+        return primaryDomain;
+#endif
+
+    return hostString;
+}
+
+WallTime ResourceLoadStatistics::reduceTimeResolution(WallTime time)
+{
+    return WallTime::fromRawSeconds(std::floor(time.secondsSinceEpoch() / timestampResolution) * timestampResolution.seconds());
+}
+
 }