/* * 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 * 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. ``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 * 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" #include "ResourceLoadStatistics.h" #include "KeyedCoding.h" #include "PublicSuffix.h" #include #include #include #include namespace WebCore { static Seconds timestampResolution { 5_s }; typedef WTF::HashMap, HashTraits>::KeyValuePairType ResourceLoadStatisticsValue; static void encodeHashSet(KeyedEncoder& encoder, const String& label, const String& key, const HashSet& hashSet) { if (hashSet.isEmpty()) return; encoder.encodeObjects(label, hashSet.begin(), hashSet.end(), [&key](KeyedEncoder& encoderInner, const RegistrableDomain& domain) { encoderInner.encodeString(key, domain.string()); }); } template static void encodeOptionSet(KeyedEncoder& encoder, const String& label, const OptionSet& optionSet) { if (optionSet.isEmpty()) return; uint64_t optionSetBitMask = optionSet.toRaw(); encoder.encodeUInt64(label, optionSetBitMask); } #if ENABLE(WEB_API_STATISTICS) static void encodeHashSet(KeyedEncoder& encoder, const String& label, const String& key, const HashSet& hashSet) { if (hashSet.isEmpty()) return; encoder.encodeObjects(label, hashSet.begin(), hashSet.end(), [&key](KeyedEncoder& encoderInner, const String& origin) { encoderInner.encodeString(key, origin); }); } static void encodeFontHashSet(KeyedEncoder& encoder, const String& label, const HashSet& 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); }); }); } #endif void ResourceLoadStatistics::encode(KeyedEncoder& encoder) const { encoder.encodeString("PrevalentResourceDomain"_s, registrableDomain.string()); encoder.encodeDouble("lastSeen"_s, lastSeen.secondsSinceEpoch().value()); // User interaction encoder.encodeBool("hadUserInteraction"_s, hadUserInteraction); encoder.encodeDouble("mostRecentUserInteraction"_s, mostRecentUserInteractionTime.secondsSinceEpoch().value()); encoder.encodeBool("grandfathered"_s, grandfathered); // Storage access encodeHashSet(encoder, "storageAccessUnderTopFrameDomains"_s, "domain"_s, storageAccessUnderTopFrameDomains); // Top frame stats encodeHashSet(encoder, "topFrameUniqueRedirectsTo"_s, "domain"_s, topFrameUniqueRedirectsTo); encodeHashSet(encoder, "topFrameUniqueRedirectsFrom"_s, "domain"_s, topFrameUniqueRedirectsFrom); encodeHashSet(encoder, "topFrameLinkDecorationsFrom"_s, "domain", topFrameLinkDecorationsFrom); encoder.encodeBool("gotLinkDecorationFromPrevalentResource"_s, gotLinkDecorationFromPrevalentResource); encodeHashSet(encoder, "topFrameLoadedThirdPartyScripts"_s, "domain", topFrameLoadedThirdPartyScripts); // Subframe stats encodeHashSet(encoder, "subframeUnderTopFrameDomains"_s, "domain"_s, subframeUnderTopFrameDomains); // Subresource stats encodeHashSet(encoder, "subresourceUnderTopFrameDomains"_s, "domain"_s, subresourceUnderTopFrameDomains); encodeHashSet(encoder, "subresourceUniqueRedirectsTo"_s, "domain"_s, subresourceUniqueRedirectsTo); encodeHashSet(encoder, "subresourceUniqueRedirectsFrom"_s, "domain"_s, subresourceUniqueRedirectsFrom); // Prevalent Resource encoder.encodeBool("isPrevalentResource"_s, isPrevalentResource); encoder.encodeBool("isVeryPrevalentResource"_s, isVeryPrevalentResource); encoder.encodeUInt32("dataRecordsRemoved"_s, dataRecordsRemoved); encoder.encodeUInt32("timesAccessedAsFirstPartyDueToUserInteraction"_s, timesAccessedAsFirstPartyDueToUserInteraction); encoder.encodeUInt32("timesAccessedAsFirstPartyDueToStorageAccessAPI"_s, timesAccessedAsFirstPartyDueToStorageAccessAPI); #if ENABLE(WEB_API_STATISTICS) encodeFontHashSet(encoder, "fontsFailedToLoad", fontsFailedToLoad); encodeFontHashSet(encoder, "fontsSuccessfullyLoaded", fontsSuccessfullyLoaded); encodeHashSet(encoder, "topFrameRegistrableDomainsWhichAccessedWebAPIs", "domain", topFrameRegistrableDomainsWhichAccessedWebAPIs); encodeCanvasActivityRecord(encoder, "canvasActivityRecord", canvasActivityRecord); encodeOptionSet(encoder, "navigatorFunctionsAccessedBitMask", navigatorFunctionsAccessed); encodeOptionSet(encoder, "screenFunctionsAccessedBitMask", screenFunctionsAccessed); #endif } static void decodeHashCountedSet(KeyedDecoder& decoder, const String& label, HashCountedSet& hashCountedSet) { Vector ignored; IGNORE_WARNINGS_BEGIN("unused-result") decoder.decodeObjects(label, ignored, [&hashCountedSet](KeyedDecoder& decoderInner, String& domain) { if (!decoderInner.decodeString("origin", domain)) return false; unsigned count; if (!decoderInner.decodeUInt32("count", count)) return false; hashCountedSet.add(RegistrableDomain::uncheckedCreateFromRegistrableDomainString(domain), count); return true; }); IGNORE_WARNINGS_END } static void decodeHashSet(KeyedDecoder& decoder, const String& label, const String& key, HashSet& hashSet) { Vector ignored; IGNORE_WARNINGS_BEGIN("unused-result") decoder.decodeObjects(label, ignored, [&hashSet, &key](KeyedDecoder& decoderInner, String& domain) { if (!decoderInner.decodeString(key, domain)) return false; hashSet.add(RegistrableDomain::uncheckedCreateFromRegistrableDomainString(domain)); return true; }); IGNORE_WARNINGS_END } template static void decodeOptionSet(KeyedDecoder& decoder, const String& label, OptionSet& optionSet) { uint64_t optionSetBitMask = 0; if (decoder.decodeUInt64(label, optionSetBitMask)) optionSet = OptionSet::fromRaw(optionSetBitMask); } #if ENABLE(WEB_API_STATISTICS) static void decodeHashSet(KeyedDecoder& decoder, const String& label, const String& key, HashSet& hashSet) { Vector ignore; IGNORE_WARNINGS_BEGIN("unused-result") decoder.decodeObjects(label, ignore, [&hashSet, &key](KeyedDecoder& decoderInner, String& origin) { if (!decoderInner.decodeString(key, origin)) return false; hashSet.add(origin); return true; }); IGNORE_WARNINGS_END } static void decodeFontHashSet(KeyedDecoder& decoder, const String& label, HashSet& hashSet) { decodeHashSet(decoder, label, "font", hashSet); } static void decodeCanvasActivityRecord(KeyedDecoder& decoder, const String& label, CanvasActivityRecord& canvasActivityRecord) { IGNORE_WARNINGS_BEGIN("unused-result") decoder.decodeObject(label, canvasActivityRecord, [] (KeyedDecoder& decoderInner, CanvasActivityRecord& canvasActivityRecord) { if (!decoderInner.decodeBool("wasDataRead", canvasActivityRecord.wasDataRead)) return false; Vector 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; }); IGNORE_WARNINGS_END } #endif bool ResourceLoadStatistics::decode(KeyedDecoder& decoder, unsigned modelVersion) { String registrableDomainAsString; if (modelVersion >= 15) { if (!decoder.decodeString("PrevalentResourceDomain", registrableDomainAsString)) return false; } else { if (!decoder.decodeString("PrevalentResourceOrigin", registrableDomainAsString)) return false; } registrableDomain = RegistrableDomain::uncheckedCreateFromRegistrableDomainString(registrableDomainAsString); // User interaction if (!decoder.decodeBool("hadUserInteraction", hadUserInteraction)) return false; // Storage access if (modelVersion >= 15) decodeHashSet(decoder, "storageAccessUnderTopFrameDomains", "domain", storageAccessUnderTopFrameDomains); else decodeHashSet(decoder, "storageAccessUnderTopFrameOrigins", "origin", storageAccessUnderTopFrameDomains); // Top frame stats if (modelVersion >= 15) { decodeHashSet(decoder, "topFrameUniqueRedirectsTo", "domain", topFrameUniqueRedirectsTo); decodeHashSet(decoder, "topFrameUniqueRedirectsFrom", "domain", topFrameUniqueRedirectsFrom); } else if (modelVersion >= 11) { HashCountedSet topFrameUniqueRedirectsToCounted; decodeHashCountedSet(decoder, "topFrameUniqueRedirectsTo", topFrameUniqueRedirectsToCounted); for (auto& domain : topFrameUniqueRedirectsToCounted.values()) topFrameUniqueRedirectsTo.add(domain); HashCountedSet topFrameUniqueRedirectsFromCounted; decodeHashCountedSet(decoder, "topFrameUniqueRedirectsFrom", topFrameUniqueRedirectsFromCounted); for (auto& domain : topFrameUniqueRedirectsFromCounted.values()) topFrameUniqueRedirectsFrom.add(domain); } if (modelVersion >= 16) { decodeHashSet(decoder, "topFrameLinkDecorationsFrom", "domain", topFrameLinkDecorationsFrom); if (!decoder.decodeBool("gotLinkDecorationFromPrevalentResource", gotLinkDecorationFromPrevalentResource)) return false; } if (modelVersion >= 17) { HashCountedSet topFrameLoadedThirdPartyScriptsCounted; decodeHashCountedSet(decoder, "topFrameLoadedThirdPartyScripts", topFrameLoadedThirdPartyScriptsCounted); for (auto& domain : topFrameLoadedThirdPartyScriptsCounted.values()) topFrameLoadedThirdPartyScripts.add(domain); } // Subframe stats if (modelVersion >= 15) decodeHashSet(decoder, "subframeUnderTopFrameDomains", "domain", subframeUnderTopFrameDomains); else if (modelVersion >= 14) { HashCountedSet subframeUnderTopFrameDomainsCounted; decodeHashCountedSet(decoder, "subframeUnderTopFrameOrigins", subframeUnderTopFrameDomainsCounted); for (auto& domain : subframeUnderTopFrameDomainsCounted.values()) subframeUnderTopFrameDomains.add(domain); } // Subresource stats if (modelVersion >= 15) { decodeHashSet(decoder, "subresourceUnderTopFrameDomains", "domain", subresourceUnderTopFrameDomains); decodeHashSet(decoder, "subresourceUniqueRedirectsTo", "domain", subresourceUniqueRedirectsTo); decodeHashSet(decoder, "subresourceUniqueRedirectsFrom", "domain", subresourceUniqueRedirectsFrom); } else { HashCountedSet subresourceUnderTopFrameDomainsCounted; decodeHashCountedSet(decoder, "subresourceUnderTopFrameOrigins", subresourceUnderTopFrameDomainsCounted); for (auto& domain : subresourceUnderTopFrameDomainsCounted.values()) subresourceUnderTopFrameDomains.add(domain); HashCountedSet subresourceUniqueRedirectsToCounted; decodeHashCountedSet(decoder, "subresourceUniqueRedirectsTo", subresourceUniqueRedirectsToCounted); for (auto& domain : subresourceUniqueRedirectsToCounted.values()) subresourceUniqueRedirectsTo.add(domain); if (modelVersion >= 11) { HashCountedSet subresourceUniqueRedirectsFromCounted; decodeHashCountedSet(decoder, "subresourceUniqueRedirectsFrom", subresourceUniqueRedirectsFromCounted); for (auto& domain : subresourceUniqueRedirectsFromCounted.values()) subresourceUniqueRedirectsFrom.add(domain); } } // Prevalent Resource if (!decoder.decodeBool("isPrevalentResource", isPrevalentResource)) return false; 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; double mostRecentUserInteractionTimeAsDouble; if (!decoder.decodeDouble("mostRecentUserInteraction", mostRecentUserInteractionTimeAsDouble)) return false; mostRecentUserInteractionTime = WallTime::fromRawSeconds(mostRecentUserInteractionTimeAsDouble); if (!decoder.decodeBool("grandfathered", grandfathered)) return false; 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 ENABLE(WEB_API_STATISTICS) if (modelVersion >= 13) { decodeFontHashSet(decoder, "fontsFailedToLoad", fontsFailedToLoad); decodeFontHashSet(decoder, "fontsSuccessfullyLoaded", fontsSuccessfullyLoaded); decodeHashSet(decoder, "topFrameRegistrableDomainsWhichAccessedWebAPIs", "domain", topFrameRegistrableDomainsWhichAccessedWebAPIs); decodeCanvasActivityRecord(decoder, "canvasActivityRecord", canvasActivityRecord); decodeOptionSet(decoder, "navigatorFunctionsAccessedBitMask", navigatorFunctionsAccessed); decodeOptionSet(decoder, "screenFunctionsAccessedBitMask", screenFunctionsAccessed); } #endif return true; } static void appendBoolean(StringBuilder& builder, const String& label, bool flag) { builder.append(" ", label, ": ", flag ? "Yes" : "No"); } static void appendHashSet(StringBuilder& builder, const String& label, const HashSet& hashSet) { if (hashSet.isEmpty()) return; builder.append(" ", label, ":\n"); for (auto& entry : hashSet) builder.append(" ", entry.string(), '\n'); } #if ENABLE(WEB_API_STATISTICS) static void appendHashSet(StringBuilder& builder, const String& label, const HashSet& hashSet) { if (hashSet.isEmpty()) return; builder.append(" ", label, ":\n"); for (auto& entry : hashSet) builder.append(" ", entry, '\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& optionSet) { if (optionSet.isEmpty()) return; builder.append(" navigatorFunctionsAccessed:\n"); for (auto navigatorAPI : optionSet) builder.append(" ", navigatorAPIEnumToString(navigatorAPI), '\n'); } static void appendScreenAPIOptionSet(StringBuilder& builder, const OptionSet& optionSet) { if (optionSet.isEmpty()) return; builder.append(" screenFunctionsAccessed:\n"); for (auto screenAPI : optionSet) { builder.append(" ", screenAPIEnumToString(screenAPI), '\n'); } } #endif static bool hasHadRecentUserInteraction(WTF::Seconds interactionTimeSeconds) { return interactionTimeSeconds > Seconds(0) && WallTime::now().secondsSinceEpoch() - interactionTimeSeconds < 24_h; } String ResourceLoadStatistics::toString() const { StringBuilder builder; builder.append("Registrable domain: ", registrableDomain.string(), '\n'); // User interaction appendBoolean(builder, "hadUserInteraction", hadUserInteraction); builder.append('\n'); builder.append(" mostRecentUserInteraction: ", hasHadRecentUserInteraction(mostRecentUserInteractionTime.secondsSinceEpoch()) ? "within 24 hours" : "-1"); builder.append('\n'); appendBoolean(builder, "grandfathered", grandfathered); builder.append('\n'); // Storage access appendHashSet(builder, "storageAccessUnderTopFrameDomains", storageAccessUnderTopFrameDomains); // Top frame stats appendHashSet(builder, "topFrameUniqueRedirectsTo", topFrameUniqueRedirectsTo); appendHashSet(builder, "topFrameUniqueRedirectsFrom", topFrameUniqueRedirectsFrom); appendHashSet(builder, "topFrameLinkDecorationsFrom", topFrameLinkDecorationsFrom); appendBoolean(builder, "gotLinkDecorationFromPrevalentResource", gotLinkDecorationFromPrevalentResource); builder.append('\n'); appendHashSet(builder, "topFrameLoadedThirdPartyScripts", topFrameLoadedThirdPartyScripts); // Subframe stats appendHashSet(builder, "subframeUnderTopFrameDomains", subframeUnderTopFrameDomains); // Subresource stats appendHashSet(builder, "subresourceUnderTopFrameDomains", subresourceUnderTopFrameDomains); appendHashSet(builder, "subresourceUniqueRedirectsTo", subresourceUniqueRedirectsTo); appendHashSet(builder, "subresourceUniqueRedirectsFrom", subresourceUniqueRedirectsFrom); // Prevalent Resource appendBoolean(builder, "isPrevalentResource", isPrevalentResource); builder.append('\n'); appendBoolean(builder, "isVeryPrevalentResource", isVeryPrevalentResource); builder.append('\n'); builder.append(" dataRecordsRemoved: ", dataRecordsRemoved); builder.append('\n'); #if ENABLE(WEB_API_STATISTICS) appendHashSet(builder, "fontsFailedToLoad", fontsFailedToLoad); appendHashSet(builder, "fontsSuccessfullyLoaded", fontsSuccessfullyLoaded); appendHashSet(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'); #endif return builder.toString(); } template static void mergeHashCountedSet(HashCountedSet& to, const HashCountedSet& from) { for (auto& entry : from) to.add(entry.key, entry.value); } template static void mergeHashSet(HashSet& to, const HashSet& from) { for (auto& entry : from) to.add(entry); } void ResourceLoadStatistics::merge(const ResourceLoadStatistics& other) { ASSERT(other.registrableDomain == registrableDomain); 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(storageAccessUnderTopFrameDomains, other.storageAccessUnderTopFrameDomains); // Top frame stats mergeHashSet(topFrameUniqueRedirectsTo, other.topFrameUniqueRedirectsTo); mergeHashSet(topFrameUniqueRedirectsFrom, other.topFrameUniqueRedirectsFrom); mergeHashSet(topFrameLinkDecorationsFrom, other.topFrameLinkDecorationsFrom); gotLinkDecorationFromPrevalentResource |= other.gotLinkDecorationFromPrevalentResource; mergeHashSet(topFrameLoadedThirdPartyScripts, other.topFrameLoadedThirdPartyScripts); // Subframe stats mergeHashSet(subframeUnderTopFrameDomains, other.subframeUnderTopFrameDomains); // Subresource stats mergeHashSet(subresourceUnderTopFrameDomains, other.subresourceUnderTopFrameDomains); mergeHashSet(subresourceUniqueRedirectsTo, other.subresourceUniqueRedirectsTo); mergeHashSet(subresourceUniqueRedirectsFrom, other.subresourceUniqueRedirectsFrom); // Prevalent resource stats isPrevalentResource |= other.isPrevalentResource; isVeryPrevalentResource |= other.isVeryPrevalentResource; dataRecordsRemoved = std::max(dataRecordsRemoved, other.dataRecordsRemoved); #if ENABLE(WEB_API_STATISTICS) mergeHashSet(fontsFailedToLoad, other.fontsFailedToLoad); mergeHashSet(fontsSuccessfullyLoaded, other.fontsSuccessfullyLoaded); mergeHashSet(topFrameRegistrableDomainsWhichAccessedWebAPIs, other.topFrameRegistrableDomainsWhichAccessedWebAPIs); canvasActivityRecord.mergeWith(other.canvasActivityRecord); navigatorFunctionsAccessed.add(other.navigatorFunctionsAccessed); screenFunctionsAccessed.add(other.screenFunctionsAccessed); #endif } WallTime ResourceLoadStatistics::reduceTimeResolution(WallTime time) { return WallTime::fromRawSeconds(std::floor(time.secondsSinceEpoch() / timestampResolution) * timestampResolution.seconds()); } }